Same Data = Same Page

How to ensure the hydrated Next.js page does not suddenly change

Recently I have built a "Vote for the next topic" page for my cypress.tips site where you can rank the topics for my next Cypress-related blog post or video.

The vote choices without styling

Tip: the vote calculation is done using Borda count NPM module. It is a simple points-based system: when there are N choices, the first choice gets N - 1 points, the second choice gets N - 2 points, and so on.

To prevent any bias, the list is randomly sorted. My Next.js page randomizes the topics like this

pages/vote.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const topics = {
'npm-alias': 'Useful NPM alias commands for running the app and the tests',
'connect-to-db': 'Connecting to the database from the plugin file',
'crawl-links': 'Crawling site links checking for invalid URLs',
'cy-log-tips': 'Tips for using "cy.log" command',
'doom-fixtures': 'How to avoid a pyramid of doom when loading fixtures',
}

const randomizeTopics = (topics) => {
return shuffle(topics)
}

export default function VotePage() {
// only logged in users can vote
const [session, loading] = useSession()
if (!session) {
return <VoteAccessDenied />
}

const randomizedTopics = randomizeTopics(topics)
return (
<tbody>
{Object.keys(randomizedTopics).map((key) => {
const topic = randomizedTopics[key]
return (
<tr key={key}>
<td>
<input type="number" name={key} min="1" max="5" />
</td>
<td>{topic}</td>
</tr>
)
})}
</tbody>
)
}

Everything looks right, except for the generated page. It does show the topics in the random order, but there is a weird "double" shuffle on load.

Double shuffle on load

Weird. Let's open the DevTools console. It shows an error.

Vote page console error

What is going on here?

The Next.js generates pre-rendered page, you can see it in the browser or by fetching it from the command line using httpie

HTML page returned from the server

The server ran the page JSX code, got the shuffled topics with "Crawl sites ..." at the first place and rendered the HTML. When the browser loads this page, it renders <TR><TD>Craw sites ...</TD></TR> and then Next.js starts running. It calls the VotePage function, which returns live React component ... which calls const randomizedTopics = randomizeTopics(topics) and generates a different order of topics, causing the client page to reshuffle. The rendering even catches the difference during rendering, warning us that the server and the client pages did not match.

Our server-side page was rendered from randomizeTopics(topics) and the client-side page was rendered from another call to randomizeTopics(topics), causing the mismatch.

📝 Want to see more examples of hydration and how to implement it yourself? Read my Hydrate your apps and Hydrate at build time posts.

If the randomizeTopics returned the topics in the same order, the server and the client pages would have the same HTML and there would be no problem. The static page from the server would be hydrated without the user noticing. So to solve our shuffle problem we need to ... move data generation so it happens only once on the server. For Next.js this can be done inside the getServerSideProps callback that every page can set up. In my case this function already was setting up the session, I just needed to add one more property to return.

pages/vote.js
1
2
3
4
5
6
7
8
9
10
11
export async function getServerSideProps(context) {
// an array of [key, description]
const randomizedTopics = randomizeTopics(topics)

return {
props: {
randomizedTopics,
session: await getSession(context),
},
}
}

The props are then passed to the page component

pages/vote.js
1
2
3
export default function VotePage({ randomizedTopics }) {
// render randomizedTopics
}

The final page is all good: no weird artifacts on load, no console errors, and has ok styles.

The page with fixed shuffle

The lesson: if you want to generate the same page, you have to have the same data.