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:

  1. Creates the underlying store using createStore from [src/vanilla.ts](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)
  2. Returns a hook that wraps useSyncExternalStoreWithSelector to handle selector and equality logic
  3. Accepts an optional defaultEqualityFn parameter 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:

  1. Store Creation: The factory calls createStore from [src/vanilla.ts](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) to create the underlying store API (getState, setState, subscribe).

  2. Hook Binding: The returned hook (useBoundStoreWithEqualityFn) wraps useStoreWithEqualityFn, which internally uses useSyncExternalStoreWithSelector from the use-sync-external-store shim (lines 66-80 of src/traditional.ts).

  3. Equality Resolution: The hook resolves equality functions in this priority:

    • Per-call equality function (highest priority)
    • defaultEqualityFn passed to createWithEqualityFn
    • Strict reference equality (===) when neither is provided
  4. TypeScript Support: The file defines three overloads for useStoreWithEqualityFn to ensure type safety whether you call the hook with no arguments, with a selector only, or with both selector and equality function.

Summary

  • createWithEqualityFn from zustand/traditional creates 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 useSyncExternalStoreWithSelector in [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 shallow from zustand/shallow for arrays/objects and fast-deep-equal for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →