This episode covers how to control re-renders: selectors that return primitive values, useShallow for several slices, and avoiding new objects inside selectors. You also learn transient updates with subscribe and refs to update values like a progress bar without triggering renders.

Zustand is fast because it's selective, but that speed is lost if selectors are written carelessly. Episode 13 covers how to measure and control re-renders: choosing the right state slice, using useShallow for several values, and avoiding the new-object trap that makes the store look like it always changes. We also discuss transient updates — updating values like the mouse position without re-rendering at all.
With the right combination of selectors and transient updates, the application stays responsive even when state is updated dozens of times per second.
Zustand compares selector results with strict equality. A state change only triggers a re-render if the value returned by the selector differs from before:
const count = useCounter((s) => s.count)
const name = useCounter((s) => s.user.name)useCounter((s) => s.count) returns a primitive value so the comparison is accurate. As long as count doesn't change, this component won't re-render even if other fields in the store change.
To select several values at once without excessive re-renders, use useShallow. Without useShallow, the object returned by the selector is always new, so the component re-renders continuously:
import { useShallow } from 'zustand/react/shallow'
const { count, name } = useCounter(
useShallow((s) => ({ count: s.count, name: s.name })),
)useShallow((s) => ({ ... })) compares each property with Object.is. A new object is still created, but re-rendering only happens if the values inside it truly change.
A selector that returns a new object or array every time it's called is the most common cause of endless re-renders. An example that causes trouble:
const user = useUser((s) => ({ name: s.name, age: s.age }))Even if name and age don't change, a new object literal is created each render, so strict equality always fails. The solution is to use useShallow, or select primitive values separately. The easy rule: never create an object inside a selector without useShallow.
Not every state change needs a render. Mouse position, scroll progress, or video playback can update dozens of times per second — rendering a component for every frame is wasteful. Zustand provides subscribe for the transient pattern:
import { useEffect, useRef } from 'react'
import { useProgressStore } from './stores/progress'
function ProgressBar() {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const unsubscribe = useProgressStore.subscribe(
(s) => s.progress,
(progress) => {
if (ref.current) {
ref.current.style.width = `${progress}%`
}
},
)
return unsubscribe
}, [])
return <div ref={ref} className="progress-bar" />
}useProgressStore.subscribe(selector, listener) registers a listener that runs when the progress value changes. The component renders only once; subsequent updates write directly to the DOM via the ref without going through React.
The getInitialState selector provides the initial value that subscribe returns when the listener is first called — useful for initializing refs:
useEffect(() => {
const unsubscribe = useProgressStore.subscribe(
(s) => s.progress,
(progress) => {
if (ref.current) {
ref.current.style.width = `${progress}%`
}
},
{ fireImmediately: true },
)
return unsubscribe
}, []){ fireImmediately: true } calls the listener immediately with the current value. It suits cases where the component mounts mid-process, not from the start.
Episode 13 covers Zustand's performance arsenal: selectors that pick small values, useShallow for several slices, avoiding new objects inside selectors, and transient updates that modify the DOM without rendering through subscribe plus refs.
Key takeaways:
In the next episode we will discuss SSR, React 19, and framework integration — using Zustand safely in the Next.js App Router, avoiding state mismatches during hydration, integrating the use hook in React 19, and building per-scope stores with Context.