T
TanStack4w ago
continuing-cyan

Persist Sidebar Toggle State Across SSR and Client Hydration in React with Zustand?

I have a sidebar that users can toggle on and off. To persist the sidebar’s open/closed state across page reloads, I’m using Zustand with its persist middleware. The challenge I’m facing is related to server-side rendering (SSR). Since the initial page is rendered on the server, the persisted state isn’t available during that phase. As a result, the sidebar always renders as open initially, and only after client-side hydration does Zustand apply the persisted state, causing a visible UI flicker. I’m wondering if there’s a better approach to handle this, such as storing the sidebar state in cookies or another method that works seamlessly with SSR and hydration. Has anyone dealt with this before? What are your recommendations or best practices for persisting UI state?
8 Replies
modern-teal
modern-teal4w ago
modern-teal
modern-teal4w ago
so in tanstack, maybe a server fn that reads the cookies server-side you might wanna check out this thread: https://discord.com/channels/719702312431386674/1379400567104864328 not exactly the same problem, they are dealing with persisting dark mode, but it might help
other-emerald
other-emerald4w ago
dark mode is very similar. I would persist this in local storage and read from localstorage before hydration.
continuing-cyan
continuing-cyanOP4w ago
Thanks guys for the helpful suggestion I was reading the Zustand docs and came across this https://zustand.docs.pmnd.rs/integrations/persisting-store-data#how-can-i-check-if-my-store-has-been-hydrated In my route component, I simply do not render the component if it’s not hydrated. This prevents flickering. Oops, I notice that for a split second the page appears empty…
continuing-cyan
continuing-cyanOP4w ago
other-emerald
other-emerald4w ago
never used Zustand. just want to point out we have a dehydrate and hydrate method on router you can hook into. this happens before rendering
other-emerald
other-emerald4w ago
Oh. I missed that one. Where are the docs for it? For now I’m using a silly script injection to achieve that 😅
continuing-cyan
continuing-cyanOP4w ago
Modified my implementation to make use of hydrate method available on router
export function createRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});

return routerWithQueryClient(
createTanStackRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
hydrate() {
// TODO: can we access if the router is hydrated or not in routes?
useUIStore.getState().setHasHydrated(true)
},
}),
queryClient,
)
}
export function createRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
},
},
});

return routerWithQueryClient(
createTanStackRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
hydrate() {
// TODO: can we access if the router is hydrated or not in routes?
useUIStore.getState().setHasHydrated(true)
},
}),
queryClient,
)
}
That empty page problem no longer occurs

Did you find this page helpful?