Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Optimizing Re-renders with `shallow` Equality and Store Hooks

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 shallow function 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 shallow equality function, passed as the second argument to the store hook, overrides this behavior.
  • shallow compares 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 useShallow hook 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.

Can't find a good explanation? Sign up and we'll make it for you

Sign up