Recommended resources
The freely available resources this lesson will direct you to.
Hello! Let's continue our exploration of Zustand's performance features.
In our last lesson, we analyzed how Zustand's selective subscriptions prevent unnecessary re-renders. We established that selecting primitive values is the most efficient approach, but we also identified a common challenge: selecting multiple values or a slice of an object/array using a selector like state => ({ a: state.a, b: state.b }) creates a new object on every run, which defeats the optimization.
Today, we will solve that exact problem. This lesson addresses the learning outcome: Use the shallow equality function with the store hook to optimize re-renders for components that select object or array slices.
We will cover:
- Why JavaScript's default comparison fails for object and array slices.
- What "shallow" comparison is and how it works.
- How to use Zustand's
shallowfunction to select multiple state properties efficiently.
1. The Core Problem: Referential Identity
As a quick refresher, the reason selecting an object or array slice causes re-renders is JavaScript's handling of referential identity. When a selector function returns a newly created object or array, Zustand's default equality check (Object.is, similar to ===) sees it as a new value, even if the contents are identical to the previous selection.
Fixing Redux/Zustand Re-Renders
To see a practical demonstration of this, let's watch a segment from Jack Herrington's 'Fixing Redux/Zustand Re-Renders'. He does an excellent job explaining how === works for primitives versus objects and why this leads to re-renders.
Watch the section 'Understanding Referential Identity and Re-renders' from 12:57 to 19:18. Focus on the explanation using the Quokka scratchpad, where he compares primitives, empty arrays, and empty objects.
As the video demonstrates, [] === [] is false. This is the behavior we need to override to effectively select slices of state.
2. The Solution: shallow Equality
Zustand provides a built-in solution: the shallow equality function. You can pass this function as a second argument to the useStore hook.
When you do this, Zustand stops using the default Object.is comparison and uses the shallow function instead.
What does shallow do?
It performs a "shallow" comparison of the previous and new selections.
- If the selections are objects, it checks if they have the same keys, and if the values for each key are strictly equal (
===). - If the selections are arrays, it checks if they have the same length, and if the elements at each index are strictly equal (
===).
This means it only goes one level deep. It's perfect for the common use case of selecting a flat object of state properties or a simple array of primitives.
Fixing Redux/Zustand Re-Renders
The same video from Jack Herrington demonstrates how to apply a shallow equality check. Although he first shows it in Redux, the principle is identical, and he then applies it directly to Zustand.
Watch from 19:03 to 20:45 to see how shallowEqual is used in Redux, and then watch from 27:27 to 28:22 to see the exact same pattern applied in Zustand to fix the re-render issue with an array.
3. Applying shallow to Object Slices
Now, let's see the most common pattern in action: selecting multiple properties from the store into a single object for a component.
Without shallow, this is an anti-pattern:
// This will cause re-renders on ANY state change
const { name, age } = useUserStore(
({ user }) => ({ name: user.name, age: user.age })
);
With shallow, this becomes a highly performant and recommended pattern:
import { shallow } from 'zustand/shallow';
// This will ONLY re-render if user.name or user.age changes
const { name, age } = useUserStore(
({ user }) => ({ name: user.name, age: user.age }),
shallow // The magic is here!
);
This combines the performance of atomic selection with the convenience of destructuring from a single hook call.
Upgrading React state management with zustand
The article 'Upgrading React state management with zustand' provides a clear, text-based explanation of this exact pattern.
Find the heading 'Performance optimization strategies' and read the sub-section 'Selective Data Usage'. Focus on the code block that introduces shallow and the explanation that follows it.
4. Alternative Patterns and Advanced Usage
While passing shallow as the second argument is the standard, there are other convenient and powerful ways to apply this concept. Given your experience leading teams, these patterns can be useful for establishing conventions in a larger codebase.
The useShallow Hook
Zustand now exports a useShallow hook that wraps this logic for you, making the code slightly more declarative.
// Note the import path
import { useShallow } from 'zustand/react/shallow';
// The functionality is identical to the previous example
const { name, age } = useUserStore(
useShallow(({ user }) => ({ name: user.name, age: user.age }))
);
This is often preferred for its readability.
5 Zustand BEST Practices in 5 Minutes
The '5 Zustand BEST Practices' video briefly covers this, though it refers to it as U shallow which has since been formalized into the useShallow hook. The core idea is the same.
Watch from 01:55 to 02:43. This short clip explains the problem with object selectors and introduces the useShallow hook as the solution.
Creating a Custom Selector Hook
For large applications, you might want to enforce a pattern where developers can only select state via an array of keys. This can prevent complex logic from creeping into component selectors. You can build a utility that uses shallow internally to achieve this.
How to Prevent Unnecessary Re-renders in Your React App
The article 'How to Prevent Unnecessary Re-renders in Your React App' proposes an elegant solution by creating a createStoreWithSelectors utility. This is an advanced pattern that builds an abstraction over the base hook.
Read the sections 'The Solution' and the component usage example that follows. Notice how the utility function uses shallow internally and provides the end-user with a simpler API: useBearStore(['bears', 'increment']).
This pattern is powerful because it standardizes state selection across a project, making the code more predictable and maintainable—a key concern for a team lead.
Conclusion
You have now learned the standard and most effective way to select slices of state in Zustand without causing unnecessary re-renders.
Key Takeaways:
- Selectors that create new objects or arrays will trigger re-renders on every state change by default.
- The
shallowequality function, passed as the second argument to the store hook, overrides this behavior. shallowcompares the top-level properties of an object or elements of an array, triggering a re-render only if one of them has changed.- This allows you to conveniently select multiple state properties in a single hook call while maintaining optimal performance.
- The
useShallowhook offers a more declarative syntax for the same functionality.
In our next lesson, we will build on this foundation. We'll explore how to compute derived state—values that are calculated from your existing state—directly within component selectors, and how to identify potential performance pitfalls in that process.