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:
- Captures the original
api.subscribeasorigSubscribe(line 50) - Computes the initial slice value (
currentSlice) by calling your selector on the current state - Creates a wrapper listener that recomputes the slice on every state change, compares it using
equalityFn(defaulting toObject.is), and invokes your callback only when the value genuinely changes (lines 55-62) - Supports
fireImmediately: trueto 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 toObject.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
subscribeWithSelectoris a store-mutator middleware insrc/middleware/subscribeWithSelector.tsthat overloads the basesubscribeAPI to support selector functions.- It wraps the original
subscribefromsrc/vanilla.tsand implements slice-level change detection usingObject.isby default, with support for customequalityFn. - The middleware supports three overloads: vanilla subscription, selector-based subscription, and selector-based with options (
fireImmediately,equalityFn). - In React, the
useStorehook insrc/react.tsautomatically utilizes the overloadedsubscribe, 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →