Don’t store app state in the URL with the Next.js router
Development
The Next.js team built a powerful router component, next/router. I’m not a fan (more on that at the end). It seems they followed the conventions of the already mythical React Router, but more adapted to Next.js and its features. Its API is very easy though, but it encourages relying on it for application state. Through sweat and pain I found again and again that it’s a bad idea.
Simple scenarios are okay
Imagine we have a blog app and we have a page like /blog/[postSlug].tsx
for URLs like /blog/love-hate-nextjs
We get the slug like this:
This example is fine. Blog posts are not very dynamic anyway. How frequently is a user expected to change blog post? Not that quickly.
Scenarios with fast changing state and SSR aren’t okay
However, when changing the URL too frequently between routes that have SSR, weird things can start to happen. The route change triggers a request to the server on each change and produces a re-render of the app (which can be expensive performance-wise).
As a real world example, in After Memorials users can swipe through the timeline quite rapidly, and each photo in the timeline has a unique URL associated.
If we updated the URL in each swipe, we’d quickly start seeing errors like these:
Error: Cancel rendering route
Error: Loading initial props cancelled
Also, app performance would slow down during route changes, especially when doing animations.
Solution: keep state in context/Redux and sync it to the URL after
Don’t rely on the router’s query
for rendering the application. Instead, keep your state where it should be, inside a useState
or Redux.
Then, use an effect to sync the state to the URL with a debounce, so that it will update only after there are no more changes.
Put useKeepBrowserPathUpdated
where your application state is.
In any case, I’m not a fan of next/router
. For example:
Why use
router.query
for both the URL query params and the dynamic route parameters?
Why the result of
useRouter
can change on renders? It forces us to use the undocumented global exportimport Router from 'next/router'
in order to doRouter.replace
inside auseEffect
hook?
Still, I’m very grateful to all developers for creating Next.js. It’s so good to work with it and it’s exciting to see how it’s evolving (thank you!).