When async work can overlap (awaits, polls, UI hide/show, user changes), never commit results blindly. Instead, make every async side-effect either (a) versioned (generation) or (b) in-flight locked—or remove the stale write path entirely.
Apply these rules: 1) Generation-gate state updates
2) In-flight lock for duplicate entry points
3) Prefer restructuring to avoid stale writes
4) Reset timers/flags on visibility/mode transitions
Example (generation-gated async update):
const genRef = useRef(0);
const refresh = useCallback(async () => {
const gen = ++genRef.current;
try {
const result = await desktopClient.invoke("some_call", {/* ... */});
// Drop stale responses
if (gen !== genRef.current) return;
// Safe to commit
setState(result);
} catch (e) {
if (gen !== genRef.current) return;
setError(String(e));
}
}, []);
Example (in-flight guard):
const inFlightRef = useRef(false);
const doPaste = useCallback(async (source: () => Promise<string | null>) => {
if (inFlightRef.current) return;
inFlightRef.current = true;
try {
const text = await source();
if (!text) return;
setText((prev) => prev + text);
} finally {
inFlightRef.current = false;
}
}, []);
Adopting these patterns will prevent out-of-order async responses from corrupting UI state, stop duplicate side effects, and make behavior robust under rapid user interactions and overlapping background work.