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.
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
1 | const topics = { |
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.
Weird. Let's open the DevTools console. It shows an 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
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.
1 | export async function getServerSideProps(context) { |
The props
are then passed to the page component
1 | export default function VotePage({ randomizedTopics }) { |
The final page is all good: no weird artifacts on load, no console errors, and has ok styles.
The lesson: if you want to generate the same page, you have to have the same data.