Safely handling async operations
Summary: Create a reusable async hook to fetch a unique character from the Rick and Morty API. Don't perform state updates to unmounted components. Avoid unnecessary network calls.
- This is a variation of KCD's exercise 2.3 of his Advanced React Hooks workshop. See his solution here.
- Write a component that fetches a unique character from the Rick and Morty API given a user-supplied number (the character ID).
- Add another button to fetch a random Rick and Morty character.
- If the submitted number does not correspond to a character, show the error.
- While fetching data, the input field, random button, and submit button should be disabled.
- When the number currently in the input field has been submitted has been either
resolved
orrejected
, disable the submit button unless the input the user changes it to a new value. - The user shouldn't be able to click the submit button if the character corresponding to the number in the input field is currently loaded (ie profile info shown on the screen).
- Enable the user to mount and unmount this component (via a checkbox).
- KCD's Implementation.
Important!
(The paragraph below is paraphrased from this page)
Consider the scenario where we send an http request, and before the request finishes, we change our mind and navigate to a different page (or uncheck the mount checkbox). In that case, the component would get removed from the page ("unmounted") and when the request finally does complete, because the component has been removed from the page, we’ll get this warning from React:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
⚠️❗❗⚠️ This warning should NOT pop up in our app. ⚠️❗❗⚠️
My Solution
Note: This solution doesn't use a library, but in most cases that you do. Use a library like Tanner Linsley's React Query to help do this instead of implementing everything on your own from scratch.
- The
RickAndMortyInfoCard
in the code block below uses auseSafeAsync
hook. It's responsible for managing the state, and fetching the data. - The
useSafeAsync
makes sure that the dispatch function would not run if the component is no longer mounted. The dispatch function returns the the data and state from the fetch function. - In other words, if the
RickAndMortyInfoCard
is no longer mounted, its state will no longer be updated (since it doesn't exist anymore) - The
runFunction
provided byuseSafeAsync
is the function that should run whenever data should be fetched. TherunFunction
takes in a promise and updates the state{data, status error}
. - In our case,, the promise that we feed to the
runFunction
is the return value of the fetch function we call whenever we need to fetch something.
- The
useSafeAsync
is a hook that optionally takes an initial state andreturns { status, data, error, runFunction}
. The state is just{ status, data, error}
. - The
runFunction
is a function that accepts a promise and runs a dispatch function to update the state{ status, data, error }
- This promise is assumed to be returned by the function you want to run. Example, you call it like this:
runFunction(fetchSomething(...))
wherefetchSomething(...)
returns a promise - This dispatch function is safe, meaning that the function will not run if the component that called it is unmounted
- Notice that
useSafeAsync
usesasyncReducer
anduseSafeDispatch
which i will discuss next.
asyncReducer
is a private function that is only available to useAsync
.
⚠️❗❗⚠️ WARNING ⚠️❗❗⚠️: Be careful with this, you might want to write an asyncReducer that is more explicit like how KCD implemented it.
- The
useSafeDispatch
takes a regularunsafeDispatchFunction
and returns a function that guarantees that the dispatch function (which contains the fetch function) will not be called when the component is no longer mounted. - The trick is that we have an a reference that that keeps track where a component is mounted or not. If it is not mounted then the (safe) dispatch function will not do anything.
- Bottomline:
useSafeDispatch
takes anunsafeDispatchFunction
and returns asafeDispatchFunction
- The
unsafeDispatchFunction
is unsafe because it will run regardless of whether or not the component is mounted
Finally, the top level component