The problem: When you create a store in Zustand and use it across components, any change in the store's state will cause the component to rerender, even if the component is not related to that particular state. For example, if you have a store like the one below and call useBearStore in your component, whenever the cats state is updated, your bear component will also rerender.
import create, { SetState } from "zustand";
interface BearStore {
bears: number;
cats: number;
incrementCats: () => void;
increase: (by: number) => void;
increment: () => void;
}
export const bearStore = (set: SetState<BearStore>) => ({
bears: 0,
cats: 0,
increase: (by: number) => {
set((state) => ({ bears: state.bears + by }));
},
increment: () => {
set((state) => ({ bears: state.bears += 1 }));
},
incrementCats: () => {
set((state) => ({ cats: state.cats += 1 }));
}
});
export const useBearStore = create<BearStore>(bearStore);
For example if you have a store like this and call useBearStore
in your component whenever cats state updated your bear component will rerender also.
The Solution: To prevent this issue, we can create a simple utility function using shallow in Zustand.
import { StoreApi, UseBoundStore } from "zustand";
import shallow from "zustand/shallow";
type GenericState = Record<string, any>;
export const createStoreWithSelectors = <T extends GenericState>(
store: UseBoundStore<StoreApi<T>>,
): (<K extends keyof T>(keys: K[]) => Pick<T, K>) => {
const useStore: <K extends keyof T>(keys: K[]) => Pick<T, K> = <K extends keyof T>(keys: K[]) => {
return store((state) => {
const x = keys.reduce((acc, cur) => {
acc[cur] = state[cur];
return acc;
}, {} as T);
return x as Pick<T, K>;
}, shallow);
};
return useStore;
};
Then we can update our initial store to use this utility function:
const bearStore = create<BearStore>(bearStore);
export const useBearStore= createStoreWithSelectors(bearStore);
Now we can use it in components like this:
const { bears, increment } = useBearStore(["bears", "increment"]);
With this change, the component won't rerender even if the cats state is updated.
Top comments (5)
thanks
really helpful
Amazing, however
however, will produce a type error as
_
because keys is not an array but rather an object.
The best way around that potential error is to use use a for...in loop to iterate through the keys object like so...
``export const createStoreWithSelectors = (
store: UseBoundStore>
): ((keys: K[]) => Pick) => {
const useStore: (keys: K[]) => Pick = <
K extends keyof T
};
return useStore;
};
Keys should be provided as an array, as shown in the example. Adding error handling to display a message indicating that keys should be an array would be a nice addition.
Just out of curiosity, is there a specific reason why you created this instead of using react-tracked :)
Hey, I wasn't aware of react-tracked; it looks interesting. However, I wouldn't consider using it in production for a couple of reasons:
The last published date is 9 months ago, which raises concerns about its maintenance and compatibility with newer versions of zustand.
Let's say I'm already using Zustand for managing global state in my project. If I decide to upgrade to a newer version of Zustand, I only need to consider that library. Adding another dependency like react-tracked introduces unnecessary complexity. This might not be a big issue for smaller hobby projects, but for larger projects with 50+ stores or slices, it becomes a concern.
While it's true that we shouldn't reinvent the wheel, there's a balance to strike. We should avoid adding a new library for every single detail. What I've implemented here can be achieved without creating an additional dependency, which keeps things more streamlined in my opinion.