Learning Zustand - Performance & Transient Updates
Episode 13 of 23

Learning Zustand - Performance & Transient Updates

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.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

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.

Controlling Re-renders with Selectors

The Right Selector

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:

JSAtomic selectors
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.

useShallow for Several Slices

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:

JSuseShallow compares values shallowly
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.

Avoiding New Objects on Every Render

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:

JSNew object selector on every render
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.

Transient Updates

subscribe and Refs

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:

JSProgress bar without re-rendering
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 Pattern

The getInitialState selector provides the initial value that subscribe returns when the listener is first called — useful for initializing refs:

JSgetInitialState for initial values
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.

Closing

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:

  • Strict equality determines when a component re-renders.
  • Selectors should return primitive values.
  • useShallow compares object properties shallowly.
  • An object literal inside a selector causes endless re-renders without useShallow.
  • subscribe plus refs enable transient updates without rendering.
  • fireImmediately executes the listener with the initial value on subscribe.

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.

Learning Zustand - Performance & Transient Updates | Learning Zustand