One of the trickiest challenges is state management across client and server boundaries. You have parts of your app that run on Server Components and parts that run in the Client Components. When you navigate or render a page that transitions from server to client interactivity, you may want global state (e.g., user preferences, theme, filter selections) to be shared seamlessly.
React context is commonly used to share global state. In this post we’ll walk through how to use React Context and integrate with state management tool to manage global state. In this example, I’m picking Zustand as demo. Let see the steps:
1. Define your store factory
In lib/store.ts define a factory
// lib/store.ts
import { create } from 'zustand';
export interface UIState {
theme: 'light'|'dark';
setTheme: (theme: 'light'|'dark') => void;
}
export function createUIStore(initialState?: Partial<UIState>) {
return create<UIState>((set) => ({
theme: initialState?.theme ?? 'light',
setTheme: (theme) => set({ theme }),
}));
}
On the server you’d call createUIStore() per request (or per layout). On the client you’ll also create the store and hydrate it with the initial state passed from server.
2. Pass initial state from Server omponent to Client Providers (server → client)
In your app/layout.tsx (which is a Server Component by default) read the initial theme from cookies and pass it as props to a Client-provider.
// app/layout.tsx
import { Metadata } from 'next';
import { cookies } from 'next/headers';
import UIProvider from '@/components/UIProvider';
type Theme = 'light'|'dark' | undefined;
export default async function RootLayout({ children }: { children: React.ReactNode }) {
// get the initial state from cookie. This ensures that getting the latest theme if any updates from client before that
const theme = await cookies().get('theme')?.value as Theme;
const initialUI = {
theme: theme ?? 'light',
};
return (
{/* sync store state to app theme */}
<html data-theme={theme} lang="en">
<body>
<UIProvider initialUI={initialUI}>
{children}
</UIProvider>
</body>
</html>
);
}
Define UIProvider is for client component
// components/UIProvider.tsx
'use client';
import { ReactNode, useEffect } from 'react';
import { createUIStore } from '@/lib/store';
import { StoreProvider } from '@/lib/zustand-context';
interface UIProviderProps {
initialUI: {
theme: 'light'|'dark'
};
children: ReactNode;
}
export default function UIProvider({ initialUI, children }: UIProviderProps) {
const store = createUIStore(initialUI);
useEffect(() => {
const { theme } = store.getState();
}, [store]);
return <StoreProvider store={store}>{children}</StoreProvider>;
}
Rely on a context provider StoreProvider that takes a store instance and makes it available via hooks everywhere in the client subtree
3. Use store in client components (retrieve state and do action)
If the server renders with theme “light” but your client instantly flips to “dark”, you’ll get flicker/mismatch. So, write server action to update theme into cookies (client → server). This also helps to remain state even if page is reloaded.
This is the best approach to centralize logic. Also, trying to update cookies at client, it’s not possible in case if you use secure cookies.
// actions/index.ts
'use server';
import { cookies } from 'next/headers';
export async function setCookiesTheme(theme: 'light' | 'dark') {
// Write an HTTP-only cookie the server can read next render
await cookies().set('theme', theme);
}
Access store value and action from client component
// components/SwitchTheme.tsx
'use client';
import { useStore } from '@/lib/store';
import { setCookiesTheme } from '@/actions'
export default function SwitchTheme() {
const { theme, setTheme } = useStore();
const switchTheme = (theme: 'light' | 'dark') => {
// update theme to store and cookies
setTheme(theme);
setCookiesTheme(theme);
};
return (
<>
<button onClick={switchTheme('light')}>
Light theme
</button>
<button onClick={switchTheme('dark')}>
Dark theme
</button>
</>
);
}
Reference links
- https://zustand.docs.pmnd.rs/guides/nextjs
- https://nextjs.org/docs/app/getting-started/server-and-client-components#context-providers
- https://vercel.com/guides/react-context-state-management-nextjs
- https://nextjs.org/docs/app/api-reference/functions/cookies
- https://nextjs.org/docs/13/app/building-your-application/data-fetching/forms-and-mutations
- https://zustand.docs.pmnd.rs/guides/ssr-and-hydration