Fix usePersistedState hydration mismatch for SSR#8541
Conversation
The previous implementation read from localStorage during the initial useState call. This causes React hydration mismatches because: 1. Server renders with defaultValue (no localStorage on server) 2. Client initial render reads localStorage and gets stored value 3. React sees mismatch between server HTML and client render This broke useCurrentUser which would redirect to login immediately on page load because userId was null during SSR hydration, even for logged-in users. Changes: - localStorage: defer reading to useEffect, return default initially - sessionStorage: read immediately (per-tab, no SSR concern) - Add isHydrated flag so consumers know when value is loaded - useAuthStore exposes isHydrated in authState - useCurrentUser only redirects after hydration completes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
renderPage() already awaits "About Me" text. Second call fails because both tab button and h2 heading now contain same text after hydration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| // Only redirect if auth has been hydrated from storage and there's still no userId | ||
| if (authState.isHydrated && !authState.userId) { | ||
| console.error("No user id available to get current user."); | ||
| if (typeof window !== "undefined") router.push(loginRoute); | ||
| } | ||
| return userQuery; | ||
| } |
There was a problem hiding this comment.
As far as I can see, only moving the original code inside an effect here would be a simpler fix for the issue? This should not run at the top level of the component ("the render function") in the first place since it's a side effect.
The changes to usePersistedState seems way too convoluted and over complex to me. I'm also sceptical of the hydration mismatches claim by the comments generated by Claude as the isMounted in AppRoute is meant to prevent the mismatch on initial render
There was a problem hiding this comment.
@darrenvong I don't trust claude as well, but I know very little about React. The hydration issue is real though - see my comment below
|
Also, currently before any fixes, how do we reproduce this issue? 🤔 |
Error details## Error Type Recoverable ErrorError MessageHydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:
It can also happen if the client has a browser extension installed which messes with the HTML before React loaded. https://react.dev/link/hydration-mismatch ... <Styled(Component) disableGutters={true} variant="full-screen" maxWidth={false}> <Container component="section" maxWidth="lg" sx={{display:"flex", ...}}> <MuiContainer-root as="section" ownerState={{...}} className="MuiContain..." ref={null} ...> <Styled(div)> <MuiTypography-root as="h2" ref={null} className="MuiTypogra..." sx={{...}} ...>
Code Frame129 | return (
Next.js version: 15.5.7 (Webpack) |
@WouldYouKindly Thanks for the repro steps! After looking more, I reckon a much simpler fix would be to mirror the I had a play around and that seems to have fixed things. I could push something up |
|
@darrenvong please do. I’d love to learn from your fix :) |
|
@WouldYouKindly I opened a PR on the side (#8607) with what I had in mind as a fix as I thought it might be cleaner than pushing to your branch! |

The previous implementation read from localStorage during the initial useState call. This causes React hydration mismatches because:
This broke useCurrentUser which would redirect to login immediately on page load because userId was null during SSR hydration, even for logged-in users.
Changes: