Hooks that combine optimistic UI updates with async confirmations (debounced state, deferred promises, streamed partial messages) are prone to race conditions across timing gaps and component lifecycle transitions (unmount/remount, navigation away and back). Before writing or rendering based on an async result, validate it against the current state/identity rather than trusting a stale ref, closure, or an incomplete combined condition:
Example pattern — validate identity/version before applying a late async result:
const handleSend = async (threadId: string, version: number) => {
try {
await sendPrompt(threadId);
} catch {
if (isCurrent(threadId, version)) {
restoreDraft(threadId, promptInputRef.current);
}
// else: thread was remounted/navigated away; drop the stale write
}
};
Additionally, cover these races with tests that exercise the full hook (not just a pure helper) across the relevant lifecycle sequence — e.g. mount → async event → unmount → remount → late resolution — asserting the final state is correct.