How to Implement Transient Updates Without Re-Renders in Zustand for High-Frequency State Changes

Use the store's subscribe method directly inside a useEffect hook to listen for changes, store the latest value in a useRef instead of component state, and bypass the useStore hook entirely to prevent React re-renders during high-frequency updates.

Zustand's default React integration triggers a component re-render whenever the store state changes, which creates severe performance bottlenecks for rapid updates like animations or real-time sensor data. By implementing transient updates without re-renders in Zustand, you can subscribe to state changes outside of React's rendering cycle using the vanilla store API. This pattern allows components to receive high-frequency updates silently through mutable references rather than state, keeping the UI smooth even at 60Hz or higher.

Understanding the Re-Render Problem

Zustand's core store (createStore) always notifies all listeners whenever setState changes the internal state. The React hook useStore, implemented with React.useSyncExternalStore in [src/react.ts](https://github.com/pmndrs/zustand/blob/main/src/react.ts) (lines 30-38), subscribes to these notifications to trigger component updates.

While this ensures UI consistency for most use cases, it becomes problematic for high-frequency scenarios like cursor tracking or game loops. Every call to setState iterates through the listener Set defined in [src/vanilla.ts](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) (lines 88-92), causing React to schedule a re-render each time.

Implementing Transient Updates with subscribe

The transient update pattern bypasses useStore entirely by calling the store's subscribe method directly within a useEffect. Because this subscription exists outside React's state management, the component never re-renders when the listener fires.

Store the latest value in a mutable container (useRef or a plain variable) rather than in component state. The subscription mechanism is defined in src/vanilla.ts, where setState computes nextState and, if changed, iterates listeners.forEach(listener => listener(state, previousState)).

import { create } from 'zustand'
import { useEffect, useRef } from 'react'

// Create a vanilla store (no React hook)
const useScratchStore = create((set) => ({
  scratches: 0,
  tick: () => set((s) => ({ scratches: s.scratches + 1 })),
}))

export const ScratchComponent = () => {
  // Hold the latest value in a mutable ref
  const scratchesRef = useRef(useScratchStore.getState().scratches)

  // Subscribe once on mount; update the ref on every change
  useEffect(() => {
    const unsub = useScratchStore.subscribe((state) => {
      scratchesRef.current = state.scratches          // No re-render
    })
    return unsub                                    // Auto-cleanup on unmount
  }, [])

  // Render something that does not depend on scratches
  return <div>Hover over me – values update silently.</div>
}

Because ScratchComponent never reads scratchesRef.current during the render phase, React never schedules an update, allowing the subscription to handle thousands of changes per second without performance degradation.

Optimizing with Selective Subscriptions

For complex stores where you only need a specific slice of state, the subscribeWithSelector middleware reduces unnecessary work. Implemented in [src/middleware/subscribeWithSelector.ts](https://github.com/pmndrs/zustand/blob/main/src/middleware/subscribeWithSelector.ts) (lines 50-68), this middleware allows you to attach a selector to the subscription while still avoiding React re-renders.

The listener only runs when the selected slice actually changes, using Object.is for equality checking by default.

import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import { useEffect, useRef } from 'react'

const useMouseStore = create(
  subscribeWithSelector(() => ({
    x: 0,
    y: 0,
    move: (dx: number, dy: number) =>
      set((s) => ({ x: s.x + dx, y: s.y + dy })),
  }))
)

export const MouseTracker = () => {
  const positionRef = useRef({ x: 0, y: 0 })

  useEffect(() => {
    const unsub = useMouseStore.subscribe(
      (state) => ({ x: state.x, y: state.y }),   // Selector for specific slice
      (pos) => {
        positionRef.current = pos                // Transient update
      },
      { fireImmediately: true }                  // Initialize with current value
    )
    return unsub
  }, [])

  return <canvas /* draw using positionRef.current */ />
}

This approach minimizes memory allocations and comparison overhead when tracking specific properties in high-frequency update streams.

Practical Use Cases: Animation Loops

Transient updates excel in animation contexts where you need the latest state inside a requestAnimationFrame loop without triggering React's render pipeline. The mutable reference provides immediate access to current values while the component renders only once.

import { useEffect, useRef } from 'react'
import { useMouseStore } from './mouseStore'

export const AnimatedCanvas = () => {
  const posRef = useRef({ x: 0, y: 0 })

  // Sync posRef with the store (transient subscription)
  useEffect(() => {
    const unsub = useMouseStore.subscribe(
      (s) => ({ x: s.x, y: s.y }),
      (p) => (posRef.current = p),
      { fireImmediately: true }
    )
    return unsub
  }, [])

  // Animation loop reads the mutable ref without causing renders
  useEffect(() => {
    let raf = 0
    const draw = () => {
      const { x, y } = posRef.current
      // Canvas drawing logic here
      raf = requestAnimationFrame(draw)
    }
    draw()
    return () => cancelAnimationFrame(raf)
  }, [])

  return <canvas width={800} height={600} />
}

This pattern keeps the animation smooth because the canvas renders independently of React's reconciliation cycle, even when the underlying store updates hundreds of times per second.

Summary

Frequently Asked Questions

What is the difference between useStore and store.subscribe in Zustand?

useStore is a React hook that uses useSyncExternalStore to bind the Zustand store to React's component lifecycle, triggering re-renders whenever subscribed state changes. In contrast, store.subscribe is a vanilla JavaScript method from the core store implementation that registers a callback function to run on state changes without any React integration, making it ideal for transient updates that should not cause re-renders.

Can I use transient updates with the standard create function from Zustand?

Yes. The create function returns a hook that also exposes the vanilla store methods including subscribe, getState, and setState. You can access these directly via useStore.subscribe or by exporting the store instance separately to subscribe outside of React components while still using the same store definition.

When should I use subscribeWithSelector instead of basic subscribe?

Use subscribeWithSelector when you need to listen to specific slices of a complex state object rather than the entire state. According to the implementation in src/middleware/subscribeWithSelector.ts, this middleware adds selector support and equality checking (defaulting to Object.is), ensuring your listener only fires when the selected value actually changes, reducing unnecessary computations during high-frequency updates.

Will transient updates cause memory leaks if I don't unsubscribe?

Yes. The store maintains a Set of listeners as implemented in src/vanilla.ts. If you fail to call the unsubscribe function returned by store.subscribe when the component unmounts, the listener remains in the store's internal registry, preventing garbage collection and potentially causing stale closures. Always return the unsubscribe function from the useEffect cleanup handler.

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 →