Use component-specific Zustand selectors, rather than slice-specific Zustand selectors, wherever possible.
Some of our components use selectors that select an entire Zustand slice. This isn't great for performance, since it means that every component that uses this selector will have to re-render when any property in the slice changes, even if the given component is not using that particular property.
Instead, we should use Zustand selectors that only return the properties that the given component needs. That way, the component will only re-render when the properties it cares about change.
See this example. Since we're only using a single property from the state, we can just inline the selector, and the component will only re-render when the output of that selector changes.
If we need multiple properties from the state, simply using useStore()
with a selector that grabs the properties we need will result in unnecessary rerenders:
const MyComponent = () => {
const state = useStore(state => ({
prop1: state.mySlice.prop1,
prop2: state.mySlice.prop2,
}));
// ^ this object gets defined on every call, so `state` will update when _any_
// state property changes, resulting in unnecessary re-renders
};
Instead, we should use useShallow()
inside of useStore()
. useShallow()
iterates over each top-level property in the object, and only triggers a re-render when one of those properties changes:
const MyComponent = () => {
const state = useStore(
useShallow(state => ({
prop1: state.mySlice.prop1,
prop2: state.mySlice.prop2,
})),
);
// ^ `useShallow()` iterates over this object, and only triggers a re-render
// if one of the top-level properties changes.
};
For code cleanliness, extract such selectors to above the component definition:
const myComponentSelector = (state: AllSlices) => ({
prop1: state.mySlice.prop1,
prop2: state.mySlice.prop2,
});
const MyComponent = () => {
const state = useStore(useShallow(myComponentSelector));
};
Then, since this is such a common use case, we've defined a useStoreShallow
hook that combines useStore
and useShallow
. So you can rewrite the above as:
import { useStoreShallow } from '../utils/use-store-shallow';
const myComponentSelector = (state: AllSlices) => ({
prop1: state.mySlice.prop1,
prop2: state.mySlice.prop2,
});
const MyComponent = () => {
const state = useStoreShallow(myComponentSelector);
};
For maintainability, selectors should be colocated with the component rather than being exported from the slice file.