Zustand store exhibits intermediate state flashes during async retry attempts
27.5K reputation · 12 Mar 2022, 20:48 UTC
When an async action in a Zustand store retries on failure, multiple setState calls can be issued in quick succession. The goal is to guarantee that only the final successful attempt updates the store and that no intermediate state values are emitted to subscribers, preventing UI flicker.
Constraints include avoiding duplicate writes, handling concurrent retries that may race, and ensuring the solution does not rely solely on shallow equality checks that could miss deep changes. It is also unclear whether middleware can suppress stale setState calls before they reach the store, or if a queuing mechanism is required.
How can intermediate emissions be prevented during retry races? Should middleware discard outdated updates based on a retry identifier? Is a state‑level flag sufficient to block stale writes?
1 answer
1 question comment
Use comments to ask for clarification. Post a solution as an answer.
27,525 reputation · 13 Mar 2022, 02:35 UTC
If you attach a monotonically increasing retry token to each async attempt and store it in the slice, you can wrap Zustand’s set in a small middleware that compares the incoming token with the latest one stored. When the token is older, the middleware simply returns without calling the original setter, so no state change (and thus no render) occurs for stale retries.
import { create } from 'zustand'
function retryTokenMiddleware(config) {
return (set, get, api) => {
const wrappedSet = (partial, replace) => {
const state = get()
if (typeof partial === 'object' && partial !== null && 'retryToken' in partial) {
const incoming = partial.retryToken
if (incoming !== undefined && incoming < state.retryToken) {
// stale update – ignore
return
}
}
return set(partial, replace)
}
return config(wrappedSet, get, api)
}
}
const useStore = create()
(retryTokenMiddleware((set, get) => ({
data: null,
loading: false,
retryToken: 0,
fetch: async () => {
set({ loading: true, retryToken: get().retryToken + 1 })
try {
const res = await fetchData()
set({ data: res, loading: false, retryToken: get().retryToken + 1 })
} catch (err) {
// retry logic – each attempt increments the token
set({ loading: true, retryToken: get().retryToken + 1 })
// ... retry again or throw
}
}
))))