Replies: 21 comments 64 replies
-
Thanks for the heads-up. |
Beta Was this translation helpful? Give feedback.
-
I debugged like 8 hours to find out why, whenever someone logs in to my app, all the users (even in incognito) on the first render see the last logged user. I first thought it was a next caching issue... The issue is indeed with the usage of Zustand on the react server component. I watched the YouTube video about how to share the state between server and client components and thought about the solution. Now I get the issue and thanks for writing about it. I hope there won't be many cases like mine, but I guess the video should be deleted. I highly recommend you not use Zustand for the purpose of sharing state between server and client components, because it will be cached until you do a rebuild or replace the edge and that's in most cases not wanted behavior. |
Beta Was this translation helpful? Give feedback.
-
What is the recommended way to use Zustand with RSC? |
Beta Was this translation helpful? Give feedback.
-
There's some misconception. The Zustand store is a global variable (or also known as a module state), which is why you don't need Provider. The issue/behavior is known in SSR, but it seems to get bigger with RSC. Maybe we can add a DEV-only warning for RSC. |
Beta Was this translation helpful? Give feedback.
-
Is it safe and recommended to set the client-side state from server like this?
/* Server side component */
import { ClientSideStateManager } from "@/src/store/client-test";
export default async function Page() {
const data = await fetch(...)
return (
<>
<ClientSideStateManager state={data} />
<ClientComponent />
</>
)
}
/* Client side component */
'use client'
import { useEffect } from 'react'
import { myStore } from './my-store'
export function ClientSideStateManager({ state }: any) {
useEffect(() => {
myStore.setState(state)
}, [state])
return <></>
} |
Beta Was this translation helpful? Give feedback.
-
Is there anyone checked, that second parameter |
Beta Was this translation helpful? Give feedback.
-
one solution is that fetching API in server component and passing it to client component and then to zustand store, and we use the API response directly in the server components. only issue is here the hydration of server and client state |
Beta Was this translation helpful? Give feedback.
-
If someone is looking to implement Zustand with RSC and they need to populate the initial state with some info from the server (like initial data from a fetch or so), the best solution is to wrap the client component with a useContext provider and use the new helper With this you can set a state the result of some fetch did in the server component and see the results immediately in your page. As some people said, it is not a good practice to try to populate a store from server components and pass that store into client. |
Beta Was this translation helpful? Give feedback.
-
I am referring to the title of this GH issue--meaning is it misguided in the same way that the OP has stated. |
Beta Was this translation helpful? Give feedback.
-
@ADTC thanks for pointing this out. I suppose I'll want to put some guards on our store implementation to throw an error if there is an attempt to write to the store in a server component. That would probably be the best way to prevent opening a can of worms to cross-request data leakage. |
Beta Was this translation helpful? Give feedback.
-
Several have recommended wrapping the store in a provider. From my past experience, I believe this diminishes the efficiency of referencing the store as a global variable by causing more re-renders. Have others had that experience? Question: would, say, wrapping selectors in an HOF to throw errors when they are called server-side be a good preventative? |
Beta Was this translation helpful? Give feedback.
-
I'm confused why the recommended approach is to use a context provider as opposed to passing the data from server component to client component and then setting the store there (as suggested by @davidebriscese)? Thought the whole point of Zustand was to avoid using context providers.. Can anyone enlighten me? |
Beta Was this translation helpful? Give feedback.
-
I've been reading how to use Zustand with SSR and seems quite complicated, why can't I just do what I normally do with the The use of |
Beta Was this translation helpful? Give feedback.
-
So from what I've gathered, it's relatively safe albeit a bit inefficient to make sensitive API calls in RSCs and pass the results to a useEffect() in a Client Component (in theory one that doesn't do anything other than update the Store) and plop that someone near the root of the closest RSC? I'm a bit confused why context would be needed here if you were updating a global store and parsed user specific info in the store itself (aka products[user] pattern). |
Beta Was this translation helpful? Give feedback.
-
I understand why it's like useShallow. |
Beta Was this translation helpful? Give feedback.
-
This seems straightforward: |
Beta Was this translation helpful? Give feedback.
-
I'm working on this exact issue as we speak. I agree with some people here, that a huge motivator to switching to Zustand stores was to avoid using React contexts to broadcast this store. Some of the big reasons why I (and probably others on this thread) are motivated to not use a Context is because:
probably could list more reasons, but these are the big ones for me. in case others want some solution to using global stores during SSR, this is what i had to do. define a export const store = createStore<MyStore>()((set, get, ...props) => {
const resetState = {
...initialState,
...createSliceA(set, get, ...props),
...createSliceB(set, get, ...props),
...createSliceC(set, get, ...props),
};
return {
initialize: (settings) => { // update store to its initial state },
reset: () => {
set(resetState);
},
}
}); Then, in my React app's entrypoint, render a export function InitializeStore(props) {
const isServer = typeof window === 'undefined';
const [initialize, reset] = useStore(s => [s.initialize, s.reset]);
const isInitializedRef = useRef(false);
if (isServer || !isInitializedRef.current) {
if (isServer) {
reset();
}
initialize(props);
isInitializedRef.current = true;
}
} i'm still on the fence about converting our store to a Context store b/c of the reasons mentioned. so hopefully this can help others continue to use global stores, even during SSR. |
Beta Was this translation helpful? Give feedback.
-
Is it possible to completely forbid zustand running on server? (If a user still wants it maybe a zustandDangerouslyDangerDontDoThisPlease type of a polyfill allows it to be run?) I am by pure luck here and was afraid to read the whole issue. I believe many people will not bother to read the zustand readme's warning. |
Beta Was this translation helpful? Give feedback.
-
Didn't read the whole thread, but we use |
Beta Was this translation helpful? Give feedback.
-
// 1. Create store creator function instead of direct store
const createContentStore = () =>
createWithEqualityFn<ContentStoreState>()(
temporal(
(set, get) => ({
// ... existing store implementation ...
}),
{
// ... existing temporal options ...
}
),
shallow
);
// 2. Add store provider hook
let store: ReturnType<typeof createContentStore>;
function getStore() {
if (typeof window === 'undefined') return createContentStore();
if (!store) {
store = createContentStore();
}
return store;
}
// 3. Export hook instead of direct store
export function useContentStore<T>(selector: (state: ContentStoreState) => T): T {
const store = getStore();
return useStore(store, selector);
}
// 4. Export temporal store hook
export function useContentTemporalStore<T>(
selector: (state: TemporalState<ContentStoreState>) => T
) {
const store = getStore();
return useStore(store.temporal, selector);
}
// 5. Update store access in helper functions
export const findEventWithinTime = (
timeMs: number,
): { event?: Event; } => {
const state = getStore().getState();
// ... rest of implementation
} Generic wrapper import { type StoreApi, useStore, type StateCreator } from 'zustand';
type StoreCreator<T> = (
...args: any[]
) => StoreApi<T>;
export function createSafeStore<T extends object, CustomStore extends StoreApi<T> = StoreApi<T>>(
createStoreFn: StateCreator<T> | StoreCreator<T>
) {
// Store creator function
const createStore = () => {
if (typeof createStoreFn === 'function' && createStoreFn.length === 0) {
// It's already a store creator
return createStoreFn() as CustomStore;
}
// It's a state creator function
return createStoreFn as StoreCreator<T>;
};
// Store singleton
let store: CustomStore;
// Get or create store instance
const getStore = () => {
if (typeof window === 'undefined') return createStore();
if (!store) store = createStore();
return store;
};
// Main store hook with type safety
function useStoreHook<S>(selector: (state: T) => S): S {
const store = getStore();
return useStore(store, selector);
}
// Direct state access for helper functions
const getState = () => getStore().getState();
// Return the store API
return {
useStore: useStoreHook,
getState,
getStore, // Expose for middleware access (temporal, persist, etc)
} as const;
} Example: import { temporal, type TemporalState } from 'zundo';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { persist } from 'zustand/middleware';
import { createSafeStore } from '@/lib/create-safe-store';
interface MyState {
count: number;
increment: () => void;
}
// Create store with all features
const store = createSafeStore<MyState, StoreApi<MyState> & { temporal: StoreApi<TemporalState<MyState>> }>(
createWithEqualityFn<MyState>()(
temporal(
persist(
(set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }))
}),
{ name: 'my-store' }
),
{ limit: 10 }
),
shallow
)
);
// Regular store usage
export const useMyStore = store.useStore;
// Temporal store usage
export const useTemporalStore = <S>(selector: (state: TemporalState<MyState>) => S) =>
useStore(store.getStore().temporal, selector);
// Helper function with direct state access
export const getCount = () => store.getState().count; Usage in components: 'use client'
function MyComponent() {
// ✅ Type safe
const count = useMyStore(state => state.count);
// ✅ Temporal support
const canUndo = useTemporalStore(state => state.pastStates.length > 0);
// ✅ All Zustand features work (persist, equality, etc)
const increment = useMyStore(state => state.increment);
return <button onClick={increment}>{count}</button>;
} |
Beta Was this translation helpful? Give feedback.
-
I am facing another issue is that the store provider, which uses React Context, is actually a client component anyway if I understand it right, so if I use it to wrap all my children components in layout, this can cause all the later RSC down to client components, which is pretty weird for me that since I treat zustand as a store cross the server/client components can cause issues so I use provider to make all my components go client. Is there any best practices to achieve this? Sometimes it would be hard to design the RSC and client components from the start in layout, because I might only need store in a client component which is the child of the RSC. I am also thinking that if we still use the variable zustand on client side but manually save it in global as an instance, which should be the way Vue does, to avoid declare too many stores based on the Next.js requests. |
Beta Was this translation helpful? Give feedback.
-
PSA: There's no such thing as state on server.
It's unwise to do anything that assumes there is.
I feel like there's a lot of people who believe we can use Zustand to add state management to React Server Components which are basically stateless. See this Medium article, and this highly popular YouTube video (UPDATE: This video is now private by request of community.). (Other state management systems fail in RSC because they use the useContext hook, which Zustand does not.)
But in my testing, it looks like it doesn't work as intended and it's highly misleading. In fact, this kind of misuse to add "state management" on RSC can introduce bugs that are very strange and hard to debug. See this comment, for example:
And, as per my own comment here: https://medium.com/@send2adtc/this-will-not-work-correctly-server-side-although-it-appears-to-work-5bac595f5131
Would the developers of Zustand care to clarify? If this is indeed incorrect usage, I highly recommend warning users about it, especially here in the Readme, and maybe even adding some kind of warning emission system within Zustand if it can detect being used on RSC.
Beta Was this translation helpful? Give feedback.
All reactions