The subscribeWithSelector middleware enables subscribing to specific parts of your state. This allows you to react to changes in selected values rather than the entire state, improving performance and code organization.
import { subscribeWithSelector } from 'zustand/middleware'
const useStore = create(
subscribeWithSelector((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
)
// Subscribe to just the x coordinate
const unsubscribe = useStore.subscribe(
(state) => state.position.x,
(x) => console.log('X changed to:', x)
)
Signature
subscribeWithSelector<T>(
stateCreator: StateCreator<T, [], []>
): StateCreator<T, [['zustand/subscribeWithSelector', never]], []>
Parameters
The state creator function that defines your store.
Basic Usage
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
type PositionStore = {
position: { x: number; y: number }
setPosition: (position: { x: number; y: number }) => void
}
const usePositionStore = create<PositionStore>()(
subscribeWithSelector((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
)
// Subscribe to entire position object
usePositionStore.subscribe(
(state) => state.position,
(position) => {
console.log('Position changed:', position)
}
)
// Subscribe to just x coordinate
usePositionStore.subscribe(
(state) => state.position.x,
(x) => {
console.log('X changed:', x)
}
)
Subscription Options
The subscribe method accepts options for fine-grained control:
useStore.subscribe(
(state) => state.count,
(count, previousCount) => {
console.log('Count changed from', previousCount, 'to', count)
},
{
// Custom equality function
equalityFn: (a, b) => a === b,
// Fire immediately with current value
fireImmediately: true,
}
)
Function to select the slice of state you want to subscribe to.
listener
(selectedState: U, previousSelectedState: U) => void
required
Callback invoked when the selected state changes.
equalityFn
(a: U, b: U) => boolean
default:"Object.is"
Custom equality function to determine if the selected state has changed.
If true, the listener is called immediately with the current value.
Custom Equality Function
Use custom equality for complex objects:
import { shallow } from 'zustand/shallow'
type Store = {
user: { name: string; age: number }
updateUser: (user: Partial<Store['user']>) => void
}
const useStore = create<Store>()(
subscribeWithSelector((set) => ({
user: { name: 'John', age: 30 },
updateUser: (updates) =>
set((state) => ({ user: { ...state.user, ...updates } })),
}))
)
// Only trigger when user object properties actually change
useStore.subscribe(
(state) => state.user,
(user) => console.log('User changed:', user),
{
equalityFn: shallow, // Shallow comparison instead of reference equality
}
)
Get the current value when subscribing:
useStore.subscribe(
(state) => state.count,
(count) => {
// This runs immediately and on every change
console.log('Current count:', count)
},
{ fireImmediately: true }
)
Multiple Subscriptions
Subscribe to different slices for different purposes:
type Store = {
user: { name: string; email: string }
settings: { theme: 'light' | 'dark'; notifications: boolean }
updateUser: (updates: Partial<Store['user']>) => void
updateSettings: (updates: Partial<Store['settings']>) => void
}
const useStore = create<Store>()(
subscribeWithSelector((set) => ({
user: { name: 'John', email: 'john@example.com' },
settings: { theme: 'light', notifications: true },
updateUser: (updates) =>
set((state) => ({ user: { ...state.user, ...updates } })),
updateSettings: (updates) =>
set((state) => ({ settings: { ...state.settings, ...updates } })),
}))
)
// Track user changes
useStore.subscribe(
(state) => state.user,
(user) => console.log('User updated:', user)
)
// Track theme changes
useStore.subscribe(
(state) => state.settings.theme,
(theme) => {
document.body.className = theme
}
)
// Track notification setting
useStore.subscribe(
(state) => state.settings.notifications,
(enabled) => {
if (enabled) {
console.log('Notifications enabled')
}
}
)
External State Synchronization
const useStore = create<PositionStore>()(
subscribeWithSelector((set) => ({
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
}))
)
// Sync with DOM
const dot = document.getElementById('dot') as HTMLDivElement
useStore.subscribe(
(state) => state.position,
(position) => {
dot.style.transform = `translate(${position.x}px, ${position.y}px)`
},
{ fireImmediately: true }
)
// Track movement
dot.addEventListener('mouseenter', () => {
const x = Math.random() * window.innerWidth
const y = Math.random() * window.innerHeight
useStore.getState().setPosition({ x, y })
})
Derived Values
Subscribe to computed/derived values:
type CartStore = {
items: Array<{ id: string; price: number; quantity: number }>
addItem: (item: CartStore['items'][0]) => void
}
const useCartStore = create<CartStore>()(
subscribeWithSelector((set) => ({
items: [],
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
}))
)
// Subscribe to total price
useCartStore.subscribe(
(state) => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
(total) => {
console.log('Total price:', total)
}
)
// Subscribe to item count
useCartStore.subscribe(
(state) => state.items.length,
(count) => {
document.title = `Cart (${count})`
}
)
Cleanup
Always clean up subscriptions:
function useEffect() {
const unsubscribe = useStore.subscribe(
(state) => state.value,
(value) => console.log(value)
)
return () => {
unsubscribe() // Clean up on unmount
}
}
Composing with Other Middleware
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware'
const useStore = create<Store>()(
devtools(
persist(
subscribeWithSelector((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
})),
{ name: 'counter-storage' }
),
{ name: 'CounterStore' }
)
)
// Still works with middleware composition
useStore.subscribe(
(state) => state.count,
(count) => console.log('Count:', count)
)
Without subscribeWithSelector, you’d need to subscribe to entire state:
// Without subscribeWithSelector - less efficient
const useStore = create((set) => ({ /* ... */ }))
useStore.subscribe((state) => {
// This runs on EVERY state change
if (state.position.x !== previousX) {
console.log('X changed:', state.position.x)
}
})
// With subscribeWithSelector - more efficient
const useStore = create(
subscribeWithSelector((set) => ({ /* ... */ }))
)
useStore.subscribe(
(state) => state.position.x,
(x) => console.log('X changed:', x) // Only runs when x changes
)