TT Lab
Get started
Learn Learning paths Courses

I searched for a whale, but got a cat

A cancel button cannot stop time travel

Continue in TT Lab

In one line

Canceling work that is no longer needed and preventing a late-arriving result from changing the screen are separate responsibilities. Design an effect's setup and cleanup as a pair.

Why this was needed

You called abort on the cat request before sending the whale search. Is it safe now? Even if the network request has stopped, post-processing for a response that has already finished may remain, and the asynchronous tool you use may not support cancellation signals. If you entrust the correctness of the screen to the mere expectation that the other side will honor the cancellation, the problem just moves to another place.

The transport model in this experiment records that a cancellation signal was received. If you turn on the checkbox, it can still deliver the response afterward. This does not pretend to be a real HTTP implementation; it is a deliberately controlled Promise model so that you can tell cancellation and ignoring results apart. You can repeat the same order without waiting for the network to happen to slow down.

How it works

When the effect for request A is set up, a validity period belonging only to A begins. Before the effect for request B is set up after a dependency changes, A's cleanup runs. Cleanup is also needed when the component disappears. Cleanup answers two questions.

  1. May the result this effect produced still be applied to the screen?
  2. Is there still any need to run the external work this effect started?

The first can be handled with an active flag or a request identifier. The second is handled with AbortController and a tool that supports cancellation signals. Whichever implementation you choose, requests must not wrongly share state with one another. If an old request's cleanup cancels the latest request, the more new searches a user makes, the more responses they lose.

Blocking only the success path is not enough. If A fails late and B has already succeeded, A's catch can show an error on B's screen. Conversely, even if A's success arrives after B's latest failure, the latest error description must not disappear. Both success and failure must follow the same validity policy.

Cleanup approach Overwriting by a past response Canceling unnecessary work
Ignore only the result Can be prevented Needs separate handling
Request only cancellation Depends on the tool's cancellation guarantee Possible with tools that support it
Validity check plus cancellation Handles both responsibilities explicitly Handles both responsibilities explicitly

Consider an empty search term too. If the user clears the input and submits, the new screen is idle. If a response sent earlier arrives and restores results, it goes against the user's last intent. Even when the same word is submitted again, it should be possible to retry if the submission number changes.

Where to put active

If you call one variable shared by all requests active, the name may be right but the lifetime can be wrong. If you change it to false while cleaning up A and then change the same variable to true while starting B, the late-arriving A also reads true. B's start has erased A's end marker. You need each effect setup to capture its own variable, or a method that compares the request number carried by the response with the current request number.

Let's trace this difference by hand. The flag created in A's setup is read by A's then and catch. A's cleanup changes only that flag to false. B's setup creates a separate flag, and B's callbacks read that one. Even if the two callbacks use variables of the same name, they are not the same storage. Look at the declaration site and what the closure captured rather than at the variable name in the code.

The request-number approach has a pitfall too. If you don't increment the current number and compare only the same search-term string, you can't tell apart two submissions of the same word. The old result of the first request can come in as if it were the result of the second retry. Include in your checks not only reverse-order responses for different words but also the case of submitting the same word consecutively.

When verifying the effect of cleanup, don't just wait blindly for the response to complete. A cancellation signal can be checked even if the request doesn't finish. Deliberately complete the past response later to see the screen protection, and separately read aborted to confirm resource cleanup. Splitting what you observe also lets you explain more precisely which responsibility was missing.

What it looks like in the field

In development StrictMode, you can observe an effect's setup, cleanup, and re-setup. If the work from the first setup isn't cleaned up, it remains alongside the second setup. Rather than removing the cleanup check to avoid this, verify that only the necessary work stays alive even when setup and cleanup repeat. Don't assume that a production build runs the same number of times as the development check.

The absence of a memory leak warning doesn't mean cleanup happened. Even when no state update is visible after the screen has disappeared, requests and event subscriptions may still be alive. In your checks, observe not only the DOM result but also the aborted state of the signal you passed. The event of the user closing the window must be connected to the end of resource usage.

What to check yourself

In the mode that ignores cancellation, deliver success and failure responses in reverse order, each separately. Then, in the mode that honors cancellation, unmount the search screen. Check separately that the result is correct and that the cancellation signal was delivered, and find out which check a wrong answer that implements only one of the two fails.

For the principle, see React's explanation of effect cleanup. The manual transport model and event numbers in this experiment are a design for learning.