How to Use Zustand subscribeWithSelector for Granular State Subscriptions

subscribeWithSelector is a store-mutator middleware in Zustand that extends the base subscribe API to let you listen to specific state slices, custom equality functions, and immediate invocation options.

The subscribeWithSelector middleware in the pmndrs/zustand repository solves the performance problem of unnecessary re-renders and callback executions by allowing components and vanilla listeners to subscribe to derived values rather than the entire state object.

What Is Zustand subscribeWithSelector?

subscribeWithSelector is a store-mutator middleware that intercepts the store creation process and replaces the default subscribe method with an overloaded version capable of handling selector functions. According to the source code in src/middleware/subscribeWithSelector.ts, the middleware registers itself using the mutator identifier ['zustand/subscribeWithSelector', never] (lines 10-13), which allows TypeScript to automatically augment the store's type definitions.

The middleware wraps the original subscribe implementation from src/vanilla.ts (lines 13-14) and provides three distinct overloads for different subscription patterns.

How subscribeWithSelector Works Under the Hood

The core logic resides in src/middleware/subscribeWithSelector.ts (lines 50-73). When you provide a selector function, the middleware:

  1. Captures the original api.subscribe as origSubscribe (line 50)
  2. Computes the initial slice value (currentSlice) by calling your selector on the current state
  3. Creates a wrapper listener that recomputes the slice on every state change, compares it using equalityFn (defaulting to Object.is), and invokes your callback only when the value genuinely changes (lines 55-62)
  4. Supports fireImmediately: true to trigger the listener immediately with the current slice value (lines 63-65)

This architecture ensures that listeners only execute when their specific slice of interest changes, eliminating the need for manual comparison logic in consumer code.

Using subscribeWithSelector in Vanilla JavaScript

For non-React applications, you can create a store with granular subscriptions using the vanilla build:

import { createStore } from 'zustand/vanilla';
import { subscribeWithSelector } from 'zustand/middleware';

type PositionStore = {
  position: { x: number; y: number };
  setPosition: (pos: { x: number; y: number }) => void;
};

const positionStore = createStore<PositionStore>()(
  subscribeWithSelector((set) => ({
    position: { x: 0, y: 0 },
    setPosition: (pos) => set({ position: pos }),
  }))
);

// Subscribe to just the x-coordinate
const unsubscribe = positionStore.subscribe(
  (state) => state.position.x,
  (x, previousX) => {
    console.log(`x changed from ${previousX} to ${x}`);
  }
);

// Update both coordinates - listener only fires if x actually changes
positionStore.getState().setPosition({ x: 5, y: 10 });

The selector (state) => state.position.x ensures the callback only executes when the x value changes, even if y updates frequently.

Using subscribeWithSelector with React

In React applications, the useStore hook in src/react.ts (lines 26-34) automatically leverages the overloaded subscribe method when you pass a selector:

import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import shallow from 'zustand/shallow';

type Todo = { id: number; text: string; completed: boolean };
type TodoStore = {
  todos: Todo[];
  toggle: (id: number) => void;
};

export const useTodoStore = create(
  subscribeWithSelector<TodoStore>((set, get) => ({
    todos: [
      { id: 1, text: 'Learn Zustand', completed: false },
      { id: 2, text: 'Write docs', completed: false },
    ],
    toggle: (id) =>
      set((state) => ({
        todos: state.todos.map((t) =>
          t.id === id ? { ...t, completed: !t.completed } : t
        ),
      })),
  }))
);

function TodoList() {
  // Only re-render when the array reference changes (shallow comparison)
  const todos = useTodoStore(
    (s) => s.todos,
    shallow
  );

  const toggle = useTodoStore((s) => s.toggle);
  
  return (
    <ul>
      {todos.map((t) => (
        <li 
          key={t.id} 
          onClick={() => toggle(t.id)} 
          style={{ textDecoration: t.completed ? 'line-through' : 'none' }}
        >
          {t.text}
        </li>
      ))}
    </ul>
  );
}

When useTodoStore receives a selector, the underlying subscribeWithSelector middleware ensures the component only re-renders when the selected slice changes, not when unrelated state updates occur.

Advanced Options: Custom Equality and Immediate Invocation

The middleware supports two optional parameters in the third argument:

  • equalityFn: A function to determine if the selected value has changed (defaults to Object.is)
  • fireImmediately: Boolean to trigger the listener immediately with the current value
import { shallow } from 'zustand/shallow';

// Using the subscribe method directly with options
useTodoStore.subscribe(
  (s) => s.todos,
  (todos) => console.log('Todos updated:', todos),
  { 
    equalityFn: shallow, 
    fireImmediately: true 
  }
);

This pattern is particularly useful when integrating with external libraries or when you need to synchronize Zustand state with non-React systems.

Composing subscribeWithSelector with Other Middleware

subscribeWithSelector composes seamlessly with other Zustand middlewares like devtools and persist because each middleware returns a state-creator function that respects the mutator signature:

import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';

type Counter = {
  count: number;
  inc: () => void;
};

export const useCounter = create(
  devtools(
    persist(
      subscribeWithSelector<Counter>((set) => ({
        count: 0,
        inc: () => set((s) => ({ count: s.count + 1 })),
      })),
      { name: 'counter-storage' }
    ),
    { name: 'counter-devtools' }
  )
);

The final store still exposes the overloaded subscribe method from subscribeWithSelector, allowing you to use granular subscriptions even with persistence and Redux DevTools enabled.

Summary

  • subscribeWithSelector is a store-mutator middleware in src/middleware/subscribeWithSelector.ts that overloads the base subscribe API to support selector functions.
  • It wraps the original subscribe from src/vanilla.ts and implements slice-level change detection using Object.is by default, with support for custom equalityFn.
  • The middleware supports three overloads: vanilla subscription, selector-based subscription, and selector-based with options (fireImmediately, equalityFn).
  • In React, the useStore hook in src/react.ts automatically utilizes the overloaded subscribe, enabling component-level granularity without extra configuration.
  • The middleware composes with devtools, persist, and other middlewares because it respects the state-creator mutator pattern.

Frequently Asked Questions

What is the difference between subscribe and subscribeWithSelector in Zustand?

The base subscribe method in src/vanilla.ts notifies listeners on every state change, requiring manual filtering inside the callback. subscribeWithSelector, implemented in src/middleware/subscribeWithSelector.ts, extends the API to accept a selector function that extracts a specific slice of state, only invoking the listener when that derived value actually changes according to the specified equality function.

How do I prevent unnecessary re-renders when using Zustand subscribeWithSelector?

Pass a selector function that returns only the specific state slice your component needs, rather than the entire state object. For React components using the useStore hook, the subscribeWithSelector middleware automatically ensures the component only re-renders when the selected slice changes. For complex objects or arrays, pass a custom equalityFn like shallow from zustand/shallow to perform reference or shallow equality checks instead of the default Object.is deep comparison.

Can I use subscribeWithSelector with the React useStore hook?

Yes. When you wrap your store with subscribeWithSelector, the useStore hook in src/react.ts automatically detects the overloaded subscribe method. Simply pass a selector function as the first argument to useStore, and the hook will use the granular subscription mechanism internally, calling React.useSyncExternalStore with the selector-aware subscription.

Does subscribeWithSelector work with Zustand persistence middleware?

Yes, subscribeWithSelector composes seamlessly with persist, devtools, and other middlewares. Because subscribeWithSelector follows the state-creator mutator pattern and returns a modified state creator, you can nest it within persist or devtools calls. The final store retains the overloaded subscribe method, allowing you to use granular subscriptions even while persisting state to localStorage or inspecting actions in Redux DevTools.

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 →