Why Zustand has useShallow (and how it prevents unnecessary renders)
Zustand is clean, flexible, and composable. But like many “unopinionated” libraries, it also gives you enough rope to shoot yourself in the foot.
One classic footgun? Selecting multiple store values as an object in useStore
— and getting re-renders even when nothing changed.
This post walks through why that happens, how to reproduce it, how useShallow
solves it, and what happens when selecting objects.
The problem: object selectors are recreated every render #
Let’s say you have a Zustand store like this:
|
|
And a component that selects both count
and user
:
|
|
Clicking the button updates only count
, but because the selector returns a new
object every time, Zustand triggers a re-render — even if user
didn’t change.
The fix: shallow comparison #
Zustand lets you pass a custom comparison function as a second argument to
useStore
. The library provides a helper called shallow
:
|
|
Now Zustand will shallow-compare the keys (count
, user
) and only re-render
if one of them changes.
The better fix: useShallow #
As of Zustand v4.4.0, you can use useShallow
for a cleaner syntax:
|
|
Same behavior, less boilerplate.
What if you select an object? #
This works fine:
|
|
Zustand tracks the reference to user
. If the object is replaced, the
component re-renders. If it’s mutated in place, it won’t:
|
|
So: when selecting objects, always ensure you’re replacing them immutably.
Alternative: split selectors #
If you want fine-grained control, you can split the selectors:
|
|
This avoids the object reference problem entirely. Zustand tracks count
and
user
separately. You don’t need shallow
in this case.
TL;DR #
- Zustand compares selector results by reference
- Returning
{ count, user }
creates a new object each render - Use
shallow
oruseShallow
to prevent unnecessary re-renders - When selecting objects, replace them immutably
- When in doubt: split selectors or wrap with
useShallow
|
|
Stop wasting renders. Use useShallow
.