Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to the fifth module of our course on Zustand.
In our last lesson, we established a crucial pattern for using Zustand in a Next.js SSR environment. We moved away from a flawed global singleton store and implemented a transient, per-request store using a store factory and a React Context provider. This solved the problem of state leakage between different user requests.
Today, we will build directly on that foundation. Our goal is to hydrate the client-side store with server-provided initial state in a Next.js application. We'll connect the final dots in our SSR setup, ensuring that data fetched on the server is correctly passed to our isolated store, so that both the server and client start with the exact same state, preventing hydration errors.
1. The Hydration Challenge: A Quick Recap
In the previous lesson, we created a store factory that looked something like this:
// src/stores/counter-store.ts
import { createStore } from 'zustand/vanilla'
// ... types ...
export const createCounterStore = (
initState: CounterState = defaultInitState
) => {
return createStore<CounterStore>()((set) => ({
...initState,
// ... actions
}))
}
The key piece for today's lesson is the initState parameter. We defined it but didn't use it. This parameter is the entry point for server-fetched data.
The term hydration in this context refers to the process where the client-side React application takes the server-rendered HTML and "brings it to life" by attaching event listeners and initializing its state. For this to work without errors, the initial state on the client must perfectly match the state used to render the HTML on theserver. Our task is to ensure our Zustand store is initialized with this server state.
The overall flow is:
- Fetch data on the server (e.g., in
getServerSideProps). - Pass this data to the page component as props.
- Pass these props into our
StoreProvider. - The
StoreProvideruses the props to initialize the store via theinitStateparameter.
2. The Concept of Hydration in SSR
This pattern of fetching data on the server, passing it as props, and using it to initialize a client-side state manager is a standard practice in SSR frameworks. Different libraries have different APIs, but the principle is the same.
To see a clear parallel, let's look at how this is done with Redux.
NextJS + State Management = Good Idea???
This video by Jack Herrington demonstrates state management in Next.js. We'll watch the section on Redux, as it provides a very clear illustration of the server-to-client state hydration pattern that we are about to implement for Zustand.
Please watch the segment from 23:17 to 26:23. Focus on the flow in getServerSideProps: data is fetched, an action is dispatched to a server-side store instance, and then store.getState() is passed as an initialState prop. On the client, a useEffect hook dispatches a 'rehydrate' action using this prop. This is the core hydration concept.
The Redux example shows the exact data flow we need: getServerSideProps -> props -> client-side store initialization. With our per-request store pattern, Zustand allows for an even more elegant solution that doesn't require a useEffect hook.
3. Implementing Hydration with the Provider Pattern
Let's modify the provider pattern from our last lesson to handle the initial state.
Step 1: Update the Store Provider
First, we need to update our CounterStoreProvider to accept the initial state as a prop and use it when creating the store. The key is that we will now initialize the store with data instead of with the default empty state.
Since we are using useRef to ensure the store is created only once, this initialization will happen on the very first render of the provider, both on the server and on the client.
The official Zustand documentation for Next.js alludes to this pattern. We will now implement the necessary changes to our provider based on the principles outlined in the docs.
Please review the section titled 'Initializing the store'. While the code example is incomplete in the resource, the description is key: it explains that we must initialize the store with a server-provided state. We will now write the code that accomplishes this, modifying the provider from the previous lesson.
Here is the updated CounterStoreProvider. Notice the new initStore prop and how it's used.
// src/providers/counter-store-provider.tsx
'use client'
import { type ReactNode, createContext, useRef, useContext } from 'react'
import { useStore } from 'zustand'
import {
type CounterStore,
createCounterStore,
type CounterState // Import CounterState type
} from '@/stores/counter-store'
export type CounterStoreApi = ReturnType<typeof createCounterStore>
export const CounterStoreContext = createContext<CounterStoreApi | undefined>(
undefined,
)
export interface CounterStoreProviderProps {
children: ReactNode
// Add a prop to accept the initial state
initState: CounterState
}
export const CounterStoreProvider = ({
children,
initState, // Destructure the new prop
}: CounterStoreProviderProps) => {
const storeRef = useRef<CounterStoreApi>()
// Create the store only once
if (!storeRef.current) {
// Use the initState prop when creating the store
storeRef.current = createCounterStore(initState)
}
return (
<CounterStoreContext.Provider value={storeRef.current}>
{children}
</CounterStoreContext.Provider>
)
}
export const useCounterStore = <T,>(
selector: (store: CounterStore) => T,
): T => {
// ... (this hook remains the same as the previous lesson)
}
Key Changes:
- The
CounterStoreProviderPropsinterface now includesinitState. - The
CounterStoreProvidercomponent acceptsinitStateas a prop. - Crucially,
createCounterStore(initState)is called, passing the server-provided state directly into our store factory.
Step 2: Fetch Data and Pass it to the Provider
Now, let's use this in a Next.js page. We'll use getServerSideProps to fetch some data (or just create it, for this example) and pass it to our provider.
For this example, let's assume we are using the Pages Router, as it provides a clear way to pass props from data fetching functions to the whole app via _app.tsx.
First, in your page file:
// src/pages/index.tsx
export async function getServerSideProps() {
// Imagine fetching this data from an API
const initialCount = 5;
return {
props: {
// This object will be passed to the page component as props,
// and will be available in _app.tsx's pageProps.
initialState: { count: initialCount },
},
};
}
// The rest of your page component...
export default function HomePage() {
// ...
}
Next, you connect this in your _app.tsx file. You'll wrap your application in the CounterStoreProvider and pass the initialState from pageProps.
// src/pages/_app.tsx
import type { AppProps } from 'next/app'
import { CounterStoreProvider } from '@/providers/counter-store-provider'
export default function App({ Component, pageProps }: AppProps) {
// Extract the initial state from pageProps
const { initialState } = pageProps
return (
<CounterStoreProvider initState={initialState}>
<Component {...pageProps} />
</CounterStoreProvider>
)
}
And that's it! The loop is closed.
- On the server:
getServerSidePropsruns, creating{ initialState: { count: 5 } }. TheAppcomponent renders, theCounterStoreProviderreceives this state and creates a store instance wherecountis 5. The HTML is rendered with the value 5. - On the client: The browser receives the HTML and the
pageProps(includinginitialState) inside the__NEXT_DATA__JSON. React hydrates. TheAppcomponent renders, theCounterStoreProviderreceives the exact sameinitialStateand creates a client-side store wherecountis also 5.
Because the initial state is identical, the client render matches the server render, and hydration succeeds without errors.
Conclusion
In this lesson, you've learned how to complete the SSR pattern for Zustand by hydrating the client-side store.
Key Takeaways:
- Hydration is the process of initializing the client-side state to match the server-side state, preventing UI mismatches.
- The
initStateparameter in our store factory is the designated entry point for server-provided data. - The pattern involves modifying the Store Provider to accept an initial state prop.
- In a Next.js page, you fetch data in
getServerSideProps/getStaticProps, return it as a prop, and pass this prop to your provider at the root of your application (_app.tsxorlayout.tsx).
By combining the per-request store pattern from the last lesson with the hydration technique from this one, you now have a complete, robust, and safe solution for using Zustand with Next.js Server-Side Rendering.
Preview of the Next Lesson
We've spent two lessons building a fairly involved architecture to make Zustand work safely with SSR. But is this always necessary? In the next lesson, we will contrast the transient store pattern for SSR with the much simpler singleton store pattern used for Client-Side Rendering (CSR). This will clarify when you need the full SSR setup and when you can use Zustand's default, simpler API.