blog/zustand-and-react-context #104
Replies: 31 comments 50 replies
|
Thanks for sharing this approach. Having the ability to mock the store out at a react level vs a global or internal level makes a lot of sense for easier testing. I often suggest zustand as an alternative to redux and I'll suggest this too. By the way, there might be a typo near here, "prop will no be passed". |
|
Isn't this exactly what is documented in the zustand readme file here: https://github.com/pmndrs/zustand?tab=readme-ov-file#react-context |
|
Cool, thanks 👍 Is there a reason why you shouldn't use the regular |
|
Thanks for this approach. Could we initialize the provider with the result of an async operation like fetching data from an API so they are initially available to the children? And if so, could this also solve hydration problems if we only render children once the data are available? |
|
I work on a social network and we use this pattern a lot so that each item in the feed has its own store, the biggest difference being the use of create instead of createStore I don't quite understand the reason for this difference in your example, as it will be turned into a hook anyway using useStore |
|
Let's say that the initialBears value could change. This could be as a result of a new fetch request happening in the component tree. How should we handle this to make sure the values are synced? Do we need an useEffect to reset the store values? |
|
What does the typescript side of this look like? My best approximation of this would be this? Anyone have a cleaner approach here? function createBearStore(initialBears: number) {
return createStore((set) => ({
bears: initialBears,
actions: {
increasePopulation: (by) =>
set((state) => ({ bears: state.bears + by })),
removeAllBears: () => set({ bears: 0 }),
},
}))
}
type BearStore = ReturnType<typeof createBearStore>
const BearStoreContext = React.createContext<BearStore | null>(null) |
|
What happen to both the |
|
How to use this with slices pattern ? |
Let say if React Forget is already been implemented, do you think initializing it with |
|
I first used this pattern when I first started using zustand in Next.js. It was annoying at first, but as I continued to use it, I realized that it's a good pattern because you can adjust the scope of the store with Context. It can be applied in various ways, such as having a Context at the top to store the data needed throughout the app, or using a Context on a page to use the data needed only on the page without worrying about initializing the store. |
|
I like the idea, I have not been able to implement it in using tankstack router file-based. |
|
With this approach - do you lose the ability to use the zustand store outside react comments? Say I have functions that run not within a react component, that utilize the function updateStateOutsideCompScope(update) {
useZustandStoreContext.setState(update)
}From my quick understanding - since this is being utilized within React Context - everything would need to be within a React Component? |
|
typescript implementation ? :3 |
|
I've used the Zustand + Context approach to improve performance in naive state management implementations that just passed a state object into the provider, causing all consumers to re-render for any change. I also added I've had rendering perf go from 150+ms per keystroke to ±6ms on a project that is still using Formik, for example, with this approach. It works because now only the field that is being interacted with re-renders on a keystroke, instead of every single field. |
|
How to type the const useBearStore = (selector) => {
const store = React.useContext(BearStoreContext)
if (!store) {
throw new Error('Missing BearStoreProvider')
}
return useStore(store, selector)
} |
|
Thanks a lot for the proposed solution! Will try in my next project |
|
Great article for zustand best practice, learnt lots from it, thanks for sharing. |
|
Thank you TkDodo, your code is awesome and helped me a lot! |
|
Here's how I'm currently using it in TypeScript with the slices pattern. I opted for the store.ts (React Context + Zustand; slices pattern)// store.ts
import * as itemA from './slices/itemA/slice.ts'
// import * as itemB from './slices/itemB/slice.ts'
// ...import more slices
export type TStore = itemA.TSlice; // itemA.TSlice & itemB.TSlice & itemA.TSlice... (imported slices)
// context
interface ContextProps extends StoreApi<TStore> {} // this line isn't necessary (ContextProps alias), but I'm keeping consistency with my other standard React Context file setups that exist in my project
const Context = createContext<ContextProps | null>(null);
// context provider (makes context accessible to children)
interface ProviderProps {
children: React.ReactNode;
// initialData: Partial<TStore>; // optional (store can be initialized with data)
}
export const ContextProviderItems = ({ children }: ProviderProps) => {
const [store] = useState(() =>
create<TStore>()((...a) => ({
...itemA.slice(...a),
// ...add more slices
}))
);
return (
<Context.Provider value={store}>
{children}
<DevToolsZustand />
</Context.Provider>
);
};
export function useStoreItems<T>(selector: (state: TStore) => T): T {
const store = useContext(Context);
if (!store) {
throw new Error('useStoreItems must be used within a ContextProviderItems');
}
return useStore(store, selector);
}if interested, my standard "generic" slices files (I use the folder name to determine which slice it is, similar to file-based routing with NextJS/Expo router): slices.ts// store.ts
import * as itemA from './slices/itemA/slice.ts'
// import * as itemB from './slices/itemB/slice.ts'
// ...import more slices
export type TStore = itemA.TSlice; // itemA.TSlice & itemB.TSlice & itemA.TSlice... (imported slices)
// context
interface ContextProps extends StoreApi<TStore> {} // this line isn't necessary (ContextProps alias), but I'm keeping consistency with my other standard React Context file setups that exist in my project
const Context = createContext<ContextProps | null>(null);
// context provider (makes context accessible to children)
interface ProviderProps {
children: React.ReactNode;
// initialData: Partial<TStore>; // optional (store can be initialized with data)
}
export const ContextProviderItems = ({ children }: ProviderProps) => {
const [store] = useState(() =>
create<TStore>()((...a) => ({
...itemA.slice(...a),
// ...add more slices
}))
);
return (
<Context.Provider value={store}>
{children}
<DevToolsZustand />
</Context.Provider>
);
};
export function useStoreItems<T>(selector: (state: TStore) => T): T {
const store = useContext(Context);
if (!store) {
throw new Error('useStoreItems must be used within a ContextProviderItems');
}
return useStore(store, selector);
} |
|
Thank you for sharing. I also have something to share that I would like to tell you about, if you need to check the time in different cities, you can visit https://timeis24.com Current local time |
|
Thank you for the article! I'm a bit confused about how the recommendation to "create multiple, small stores on a per-feature basis" is meant to fit in here (which I agree with, despite what Zustand recommends). Should you write all of this boilerplate and create a Provider for every store you want to pass props to or scope to a particular subtree? This pattern seems mandatory (?) for Next.js applications but I can see it getting messy. |
|
I’ve been following this guide, and hit a bump in the road that I find super weird; const EditComponent = () => {
const increasePopulation = useBearStore(state => state.increasePopulation)
return <button onClick={() => increasePopulation(1)}>Add 1</button>
}
const ViewComponent = () => {
const bears = useBearStore(state => state.bears)
return <>There are {bears} bears</>
}These selectors will override each other, causing the selector used in whichever component that mounts last to also be used by all others? Any pointers much appreciated! |
|
Can i do this const Parent = () => {
const [store] = useState(() => createStore());
return (
<Provider value={store}>
<Child />
</Provider>
);
};
const Child = () => {
const store = useContext();
console.log(store.getState());
return <div>child</div>;
}; |
|
Since it’s preferable to create multiple small stores for different purposes, how does that work with React context? A way I can think of is to create 1 context per store, but that leads to potentially nesting many layers of providers. |
|
Do you think using zustand in place of a local useState, purely for persistence middleware, is a good idea? I'm looking for ways to persist tanstack table states for users. Simple |
|
Defining the store inside the Provider means that you can only access it within React. If you need to call Pulling it out of the Provider means the Provider cannot initialize the state. This really shouldn't be an issue because you can just define the store in an ESM module that allows for top-level This means you can make an Where would you possibly want to access the store outside of React? You might want to trigger changes to store state on events not controlled by React (eg. window You might want to update the store within a React Router loader function, etc. These kinds of scenarios require you to have access to the store outside of React. |
|
I love this article and I keep coming back to it to refresh my memory on Zustand with Context API. Thanks for sharing this approach! I would like to ask you for an opinion. In stores that would truly be global, i.e a theme store, do you think this approach is still valid and advantageous? |
|
how this works when updating the state? |
|
Thanks a lot! This is very helpful. |
Uh oh!
There was an error while loading. Please reload this page.
Zustand stores a global and don't need React Context - but sometimes, it makes sense to combine them regardless.
https://tkdodo.eu/blog/zustand-and-react-context
All reactions