Answering the Core Question
When a retryable async action in a Zustand store fires multiple set calls in quick succession, each call immediately triggers a render. The UI therefore briefly shows the provisional state (e.g., loading:true or a partial payload) before the final successful state arrives.
Likely Explanation
Most often the action dispatches a “retrying” status, then schedules the next attempt. Each attempt calls set again, producing a cascade of intermediate states. If the retry logic is written as a plain async/await loop or inside a useEffect, every iteration will push a new state onto the store.
Confirmed Facts
- Zustand’s
set is synchronous and causes an immediate render.
- Multiple
set calls that are not batched will each produce a separate UI update.
- The built‑in
batch middleware and Immer can coalesce consecutive updates into a single render.
- Persist and subscribe middlewares can also trigger renders when the persisted slice changes.
- Using a retry identifier or timestamp is a common pattern to ignore stale updates.
Practical Steps for This Scenario
- Wrap the retry loop in a batch:
import { batch } from "zustand/middleware";
const useStore = create(batch((set, get) => ({
data: null,
loading: false,
setData: (payload) => set({ data: payload, loading: false }),
fetch: async () => {
set({ loading: true });
try {
const res = await fetchData();
set({ data: res, loading: false });
} catch (e) {
// retry logic
}
},
}))));
- Introduce a retry identifier:
const useStore = create((set) => ({
data: null,
loading: false,
retryId: 0,
fetch: async () => {
const currentId = Date.now();
set({ loading: true, retryId: currentId });
try {
const res = await fetchData();
set((state) => state.retryId === currentId ? { data: res, loading: false } : state);
} catch (e) {
// schedule next retry
set((state) => state.retryId === currentId ? { loading: true } : state);
}
},
}));
- Avoid exposing intermediate flags unless needed: If the UI only cares about the final payload, skip setting a
loading:true flag inside the retry loop and instead expose a single isLoading derived from whether an ongoing request exists.
- Verify the behavior: Add a console wrapper around
set to log timestamps and payloads during a retry run. Confirm that only the final payload reaches consumers after batching or identifier checks.
When a State‑Level Flag Is Not Enough
A flag like isRetrying only tells the UI that a retry is in progress; it does not suppress the intermediate set calls that cause flicker. The flag must be combined with either batching or a guard that discards stale updates.
Missing Diagnostic Detail
To tailor the recommendation further, could you confirm whether your store already uses any batching middleware (e.g., immer or batch) or a persistence layer that might trigger additional renders?