Zustand Middleware Execution Timing versus React Automatic Batching: Pre‑process or Post‑process?
0 reputation · 29 Mar 2021, 12:46 UTC
0 reputation · 29 Mar 2021, 12:46 UTC
Goal: Decide whether Zustand middleware should execute as a pre‑process step (before the store’s internal state is mutated) or as a post‑process step (after state is set but before subscribers are notified) to align with React 18’s automatic batching and avoid unnecessary re‑renders.
Uncertainty: The current implementation wraps setState and runs middleware logic around the state update, but the community has not settled on whether the wrapper should invoke the middleware before or after the mutation. This ordering affects when logging sees the old vs. new state, how persistence captures snapshots, and whether synchronous work adds to the render‑cycle cost under concurrent mode.
Specific questions:
32775 reputation · 29 Mar 2021, 15:36 UTC
Zustand middleware should primarily execute as a pre-process step (intercepting the setState call before the store is mutated) to maintain a predictable state flow. Because Zustand middleware wraps the set function, it naturally operates before the store notifies its subscribers.
Regarding React 18's automatic batching, the execution order of the middleware itself does not dictate whether a render is batched. Batching occurs at the boundary where the store notifies React listeners to trigger a re-render. Since middleware logic runs synchronously within the same event loop tick as the action, any state updates triggered by the middleware are automatically batched by React 18, regardless of whether the logic precedes or follows the primary mutation.
| Ordering | State Visibility | Primary Use Case | Batching Impact |
|---|---|---|---|
| Pre-process | Sees previous state | Validation, Action Filtering, Logging (Old $\rightarrow$ New) | None (Batched) |
| Post-process | Sees updated state | Persistence, Side-effect triggering, Analytics | None (Batched) |
To align with standard Zustand patterns and React's concurrent rendering, follow these implementation steps:
set function: Create a middleware that intercepts the state update.set.set: Update the store state.localStorage) immediately after the set call but before the function returns.const myMiddleware = (config) => (set, get, api) => {
return config(
(args) => {
// 1. Pre-process: Access state before mutation
const prevState = get();
set(args); // Mutate store
// 2. Post-process: Access state after mutation
const nextState = get();
console.log('Updated state:', nextState);
},
get, api
);
};
This analysis assumes the use of Zustand v4+ and React 18+. To verify the batching behavior in your specific environment, you can use the following diagnostic approach:
console.log inside a component's render body.set calls.Missing Diagnostic: Are you using asynchronous middleware (async/await)? If the middleware performs an await before calling set, the update occurs in a different tick, which may require flushSync or specific React 18 batching considerations to avoid multiple renders.
Use comments to ask for clarification. Post a solution as an answer.
32,775 reputation · 29 Mar 2021, 18:02 UTC
While the execution order (pre- vs. post-process) doesn't change whether React 18 batches the final update, it is important to consider the synchronous nature of Zustand middleware. Because middleware wraps the set function, any heavy computation or blocking logic executed during the pre-process phase happens before the store notifies subscribers.
In concurrent mode, while the render itself is batched, excessive synchronous work in the middleware can delay the initiation of that batch, potentially impacting interaction responsiveness. To verify the actual impact on your specific store, you can use the following approach:
set calls within a single async block are resulting in a single commit, regardless of middleware complexity.