How to Use Custom Equality Functions with createWithEqualityFn in Zustand
Use createWithEqualityFn from zustand/traditional to build a store that accepts either a default equality function during initialization or per-call equality functions via the hook's second argument, preventing unnecessary React re-renders when state slices are structurally equal but referentially different.
The pmndrs/zustand repository provides a specialized factory called createWithEqualityFn for developers who need fine-grained control over component re-rendering behavior. Unlike the standard create API, this utility allows you to supply custom equality functions—such as shallow or deep comparators—either globally for the entire store or individually for specific selectors.
What is createWithEqualityFn?
createWithEqualityFn is a factory function exported from zustand/traditional that constructs a bound store hook with built-in support for equality comparisons. According to the source code in [src/traditional.ts](https://github.com/pmndrs/zustand/blob/main/src/traditional.ts), the factory:
- Creates the underlying store using
createStorefrom [src/vanilla.ts](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) - Returns a hook that wraps
useSyncExternalStoreWithSelectorto handle selector and equality logic - Accepts an optional
defaultEqualityFnparameter that applies to all selector calls unless overridden
The returned hook can be invoked in three ways:
- Without arguments → returns the entire state object
- With a selector → returns the selected slice using the default equality function
- With a selector and equality function → uses the provided equality function for that specific call
Installation and Basic Setup
To use createWithEqualityFn, import it from the traditional submodule rather than the main Zustand package.
import { createWithEqualityFn } from 'zustand/traditional'
import { shallow } from 'zustand/shallow'
// Create store with optional default equality function
const useStore = createWithEqualityFn(
(set) => ({
items: [] as string[],
count: 0,
addItem: (item: string) => set((state) => ({
items: [...state.items, item],
count: state.count + 1
}))
}),
shallow // Optional: sets default equality for all selectors
)
Using Per-Call Custom Equality Functions
The most common pattern involves passing an equality function as the second argument to the hook when selecting specific state slices. This approach gives you granular control over which components re-render.
Basic Selector with Shallow Equality
Use the shallow helper from zustand/shallow when selecting arrays or objects that may have new references but identical contents.
import { shallow } from 'zustand/shallow'
function ItemList() {
// Only re-renders if items array changes shallowly
const items = useStore(state => state.items, shallow)
return (
<ul>
{items.map(item => <li key={item}>{item}</li>)}
</ul>
)
}
Deep Equality for Complex Objects
For nested objects where you need to compare values at arbitrary depths, use a deep equality function like fast-deep-equal.
import deepEqual from 'fast-deep-equal'
function UserProfile() {
// Re-renders only when user object is deeply different
const user = useStore(
state => state.user,
deepEqual
)
return <div>{user.name} - {user.age}</div>
}
Setting a Default Equality Function
When you want consistent equality behavior across your entire application without repeating the equality function in every component, pass it as the second argument to createWithEqualityFn.
Global Deep Equality Configuration
import { createWithEqualityFn } from 'zustand/traditional'
import deepEqual from 'fast-deep-equal'
const useStore = createWithEqualityFn(
(set) => ({
config: { theme: 'light', lang: 'en' },
updateConfig: (newConfig) => set({ config: newConfig })
}),
deepEqual // All selectors use deep equality by default
)
// Component uses default deep equality automatically
function ConfigDisplay() {
const config = useStore(state => state.config)
return <div>{config.theme}</div>
}
Overriding Defaults for Specific Selectors
Even with a default equality function, you can override it for specific use cases by passing a different function to the hook.
import { shallow } from 'zustand/shallow'
function SpecificComponent() {
// Override global deepEqual with shallow for this selector
const items = useStore(state => state.items, shallow)
return <div>{items.length} items</div>
}
How It Works Under the Hood
The implementation in [src/traditional.ts](https://github.com/pmndrs/zustand/blob/main/src/traditional.ts) reveals the architectural details:
-
Store Creation: The factory calls
createStorefrom [src/vanilla.ts](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) to create the underlying store API (getState,setState,subscribe). -
Hook Binding: The returned hook (
useBoundStoreWithEqualityFn) wrapsuseStoreWithEqualityFn, which internally usesuseSyncExternalStoreWithSelectorfrom theuse-sync-external-storeshim (lines 66-80 ofsrc/traditional.ts). -
Equality Resolution: The hook resolves equality functions in this priority:
- Per-call equality function (highest priority)
defaultEqualityFnpassed tocreateWithEqualityFn- Strict reference equality (
===) when neither is provided
-
TypeScript Support: The file defines three overloads for
useStoreWithEqualityFnto ensure type safety whether you call the hook with no arguments, with a selector only, or with both selector and equality function.
Summary
createWithEqualityFnfromzustand/traditionalcreates stores that support custom equality comparisons beyond strict reference equality.- You can supply a default equality function during store creation that applies to all selectors, or pass equality functions per-call for granular control.
- The implementation relies on
useSyncExternalStoreWithSelectorin [src/traditional.ts](https://github.com/pmndrs/zustand/blob/main/src/traditional.ts) to efficiently compare selected state slices and prevent unnecessary React re-renders. - Common equality strategies include
shallowfromzustand/shallowfor arrays/objects andfast-deep-equalfor nested structures.
Frequently Asked Questions
What is the difference between create and createWithEqualityFn?
The standard create API from zustand uses strict reference equality (===) to determine when selected state slices change, which can cause unnecessary re-renders when you return new object or array references that contain the same data. createWithEqualityFn, exported from zustand/traditional, allows you to supply custom equality functions—such as shallow or deep comparators—either as a store-wide default or per selector call, giving you precise control over re-render behavior.
When should I use shallow vs deep equality?
Use shallow equality (via zustand/shallow) when selecting arrays or objects where you want to compare only the first level of properties—this is ideal for lists where item order matters but nested object contents don't require comparison. Use deep equality (via libraries like fast-deep-equal) when selecting nested objects or trees where changes at any depth should trigger re-renders, though be aware that deep comparisons carry higher computational costs and should be used sparingly on large state trees.
Can I use createWithEqualityFn without TypeScript?
Yes, createWithEqualityFn works identically in JavaScript projects. Import it from zustand/traditional and pass your state creator function and optional equality function as arguments. The API surface is the same: the returned hook accepts an optional selector function and an optional equality function as its second argument.
How do I debug re-render issues with custom equality functions?
Start by verifying that your equality function actually returns true when you expect it to—log the previous and next values inside a custom equality function to ensure they're being compared correctly. Check that you're importing createWithEqualityFn from zustand/traditional rather than the standard create from zustand, as the latter doesn't support the equality function parameter. Finally, use React DevTools Profiler to confirm that components using the store hook are indeed skipping re-renders when your equality function returns true.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →