Race Conditions in Effects
Asynchronous work started by effects can finish in a different order from the order in which it was started. If an older operation updates state after a newer one, the UI can display stale data.
In a frontend effect, changing a dependency can start a new asynchronous operation such as a network request. The previous operation may still be running when the dependency changes again, so multiple operations can be in flight at once.
The race occurs when completion order differs from start order. For example, a request for `A` may start first, a request for `B` may start later, and `B` may finish first. If `A` then updates the same state, the interface can incorrectly show results for `A` even though the current input is `B`.
A common solution is to make cleanup invalidate work that belongs to an older effect run. A local `ignore` flag can prevent a stale completion from calling the state setter. For network requests that support cancellation, `AbortController` can additionally stop the obsolete request rather than merely ignoring its result.
The important distinction is that cancellation and stale-result protection solve related but different problems. Cancellation can reduce unnecessary work, while guarding the state update ensures that an operation which still completes cannot overwrite state belonging to a newer effect run.
Key points
- Effect executions can overlap when their asynchronous work has not finished before dependencies change.
- Completion order is not guaranteed to match request-start order.
- A stale-result guard prevents an older effect run from committing state.
- `AbortController` can cancel supported requests during effect cleanup.
- Cleanup should invalidate or cancel work associated with the previous effect execution.
Interview questions
Why can an effect cause a race condition when fetching data?
An effect can start asynchronous work whenever its dependencies change. A later effect execution can start before the earlier request has finished. Network latency can make the earlier request finish last. If both completions update the same state, the older response can overwrite the newer result.
How does an `AbortController` help with an effect race?
The effect creates an `AbortController` for the asynchronous request it starts. Its cleanup function calls `abort()` when that effect execution is replaced or the component unmounts. A fetch using the controller's signal is then asked to stop, and its abort rejection can be handled separately from real failures. This both reduces obsolete work and prevents the normal success path from processing the canceled request.
Is request cancellation alone sufficient to solve asynchronous races?
Not universally. Cancellation depends on whether the underlying operation supports cancellation and whether every relevant asynchronous step honors it. A stale-result guard provides a separate correctness guarantee: an obsolete effect run is not allowed to commit its result. In practice, cancellation and invalidation are often combined when the API supports cancellation.
How would you implement the stale-result guard without canceling the request?
Create a value local to each effect execution, such as an `ignore` boolean initialized to `false`. The cleanup function sets that value to `true`, and the asynchronous continuation checks it before committing state. Because each effect execution has its own closure, cleanup invalidates only that execution's work. This protects state even when the underlying operation cannot be canceled.
What would you consider when designing a reusable data-fetching hook to avoid effect races?
The hook should associate each request with the current input and ensure obsolete results cannot replace current state. It should clean up on dependency changes and unmount, preferably canceling supported requests. It also needs deliberate handling for loading, success, error, and cancellation states. For more complex applications, a data-fetching library may centralize caching, deduplication, request identity, and synchronization rather than making each component manage these concerns independently.