TT Lab
Get started
Learn Learning paths Courses

I searched for a whale, but got a cat

A search hook that rejects stale responses

Continue in TT Lab

Goal

You implement a React+TypeScript search hook yourself, and check it against stale responses, cancellation, retry, and exceptions. You practice while looking at a real React screen in the provided UI. This one lab does not stand in for HTML form writing or the whole of long-list optimization.

Why it matters

Even a successful response is a wrong screen if it differs from the current user's intent. Cancellation is resource reclamation, and ignoring a result is the correctness of the screen. If you separate the two responsibilities, you can also test asynchronous tools that don't support cancellation. A Promise rejection and a throw at call time are also different paths. You need to be able to read functions, closures, Promises, TypeScript unions, and basic React hooks.

Preparation

Run node /opt/react-workbench/prepare.mjs /root/react-search-lab lesson once at the start. If it already exists, keep using the existing copy. Do not delete or overwrite it. No runtime npm install is needed. The file to edit is /root/react-search-lab/src/useSearch.ts. In the provided src/types.ts, read the five-state contract of Query(text, revision), Search(query, signal), and SearchState. Do not change src/types.ts or the behavior of the provided search. The step examples are an initial skeleton, not the answer.

Steps

  1. Separate before a search is sent from while waiting for it — Prepare a copy of /root/react-search-lab in lesson mode. Export useSearch(request, search) from src/useSearch.ts and return a SearchState. An empty request.text is idle, and a non-empty one is loading with the corresponding query. If you clear the input and submit anew, it returns to idle. Response handling is the next step.
  2. Give different guidance for success, empty results, and failure — Call the given search(request.text, controller.signal). One or more results are success with the original items, zero is empty, and an Error rejection is error with error.message. Keep the corresponding query in each state, and after a failure a new search must be able to go through loading and succeed. Do not replace search with your own implementation.
  3. Keep the cat from taking over the whale screen — Even if you submit several searches and complete the responses in reverse order, make only the latest submission's result remain. Even if an old success arrives after the latest request failed, keep the latest error. This must hold even with a transport that ignores cancellation.
  4. Discard stale failure messages too — Even if an old failure arrives after the latest success, or an even older failure arrives after the latest failure, it must not change the screen. Preserve the latest request's Error.message as is. Keep the protection of success responses from the previous step as well.
  5. Clean up requests that are no longer needed — When the effect changes because of a new submission, make the previous request's signal.aborted become true. The latest request must be false for as long as it is needed, and true when the component is unmounted. Keep cancellation and ignoring late responses each intact.
  6. Tell apart a new intent with the same word — An identical search whose Query revision differs must also be a new request. If you complete two requests for the same word in reverse order, only the new result remains. An empty submission returns to idle, and a previous response must not be able to restore results.
  7. Handle errors thrown before a Promise is even returned — Even if search throws an Error before returning a Promise, make the whole React screen not crash and instead show error and its message. Afterward you must be able to recover normally with a new search. Keep the asynchronous rejection, cancellation, and race contracts as well.
  8. Repeat setup and cleanup, and recheck the whole contract — In development StrictMode's setup → cleanup → re-setup, check that the first request is canceled and that the second result remains even if the first response arrives late. Full grading checks steps 1–7 and this condition together. After npm run typecheck and npm run build, check reverse-order success, failure, empty results, and retry yourself in the preview on port 3000.

Notes

If you run npm run typecheck, npm run build, and npm start in that order in the working folder, you can see the preview at http://127.0.0.1:3000/. Keep the trailing /. After changing the source, rebuild and refresh the preview. In the starting step, responses may not be displayed yet. Manual delivery is a deterministic Promise model, not HTTP. The real HTTP comparison screen receives synthetic data from the lab server. Grading checks the student hook's strict types and the real React+jsdom state, and does not modify the student file. This check does not replace verification of real browser layout, performance, or screen reader usability. Each step also cumulatively checks the earlier steps. A limit of 128KiB for ordinary source files and 12 seconds for the work process applies. The final regression step can pass with earlier answers, because it re-verifies the existing contract instead of a new feature. If needed, extend the session with the +time button, and before it ends, save your modified source and observation notes separately. Files are not kept after the session ends.

Separate before a search is sent from while waiting for it

Prepare a copy of /root/react-search-lab in lesson mode. Export useSearch(request, search) from src/useSearch.ts and return a SearchState. An empty request.text is idle, and a non-empty one is loading with the corresponding query. If you clear the input and submit anew, it returns to idle. Response handling is the next step.

Separate the initial value of useState from the work of handling the submitted search in useEffect. Do not read the draft being typed in the search box directly.

Give different guidance for success, empty results, and failure

Call the given search(request.text, controller.signal). One or more results are success with the original items, zero is empty, and an Error rejection is error with error.message. Keep the corresponding query in each state, and after a failure a new search must be able to go through loading and succeed. Do not replace search with your own implementation.

Create the signal with AbortController and wire up the Promise's then and catch. Empty is not a failure but one kind of normal response.

Keep the cat from taking over the whale screen

Even if you submit several searches and complete the responses in reverse order, make only the latest submission's result remain. Even if an old success arrives after the latest request failed, keep the latest error. This must hold even with a transport that ignores cancellation.

Have the success callback check a validity value created inside each effect setup, and expire only that value in cleanup. A single true/false shared by all requests can be switched back on.

Discard stale failure messages too

Even if an old failure arrives after the latest success, or an even older failure arrives after the latest failure, it must not change the screen. Preserve the latest request's Error.message as is. Keep the protection of success responses from the previous step as well.

If you protect only then, catch can still change the screen. Check that both paths read the same effect's validity period.

Clean up requests that are no longer needed

When the effect changes because of a new submission, make the previous request's signal.aborted become true. The latest request must be false for as long as it is needed, and true when the component is unmounted. Keep cancellation and ignoring late responses each intact.

Inside the cleanup function, cancel only the controller this effect created. Ignoring the result alone does not stop the external work.

Tell apart a new intent with the same word

An identical search whose Query revision differs must also be a new request. If you complete two requests for the same word in reverse order, only the new result remains. An empty submission returns to idle, and a previous response must not be able to restore results.

If the dependencies contain only the search string, you miss a resubmission of the same word. Use the provided UI's contract that request is a new object on every submission.

Handle errors thrown before a Promise is even returned

Even if search throws an Error before returning a Promise, make the whole React screen not crash and instead show error and its message. Afterward you must be able to recover normally with a new search. Keep the asynchronous rejection, cancellation, and race contracts as well.

With search(...).catch(...), a throw from search itself never reaches the catch. Try moving the call inside a Promise callback, or handle both paths together with try/catch.

Repeat setup and cleanup, and recheck the whole contract

In development StrictMode's setup → cleanup → re-setup, check that the first request is canceled and that the second result remains even if the first response arrives late. Full grading checks steps 1–7 and this condition together. After npm run typecheck and npm run build, check reverse-order success, failure, empty results, and retry yourself in the preview on port 3000.

The last step is not a task of adding a new flag but a regression check of the cumulative implementation. Do not assume that development StrictMode's call count is the same in production.