Handling component state with React...you gotta reset it sometimes
2022-10-10
If you make a React component that has, say, a prop for a item id, and an async
action in a useEffect to fetch data for that item from an API, then you
probably also have a useState to set data after you get results back from your
API (or an error occurs). But, the interesting thing to me is
you have to remember to reset that state, including error state, when your
props change
It seems obvious, but I just wanted to write some working examples here
#Part 1: Having component state for API response or error
In the below example, we will handle fetching from the Pokemon API, and use a
useState to handle the returned data or a returned error. The important thing
to highlight is: when you go to refetch a new item from the API, you likely need
to clear the state of what was previously there (unless you want to display
stale results)
Can we make a hook to make this easier? I don't often make custom hooks, but you
can try to "encapsulate" some of the multiple-related hooks (the useStates for
error, pokemonInfo, and useEffect) into a single hook. This does not drastically
affect our approach, but in the below example, we can call
usePokemonInfo(pokemonName) and error handling and fetching is handled for us
I think it's sometimes common to forget error handling in async JS code
(useEffect async or many other contexts, etc), and there aren't e.g. lint rules
to really help, leaving errors uncaught or handled poorly. If you don't manually
handle the error in the useEffect, your user probably will not see that an
error occurred.
In addition to this error handling rant, the other point of this article is you
need to reset your component state when props change, which in the code above,
are the calls to setError(undefined) and setPokemonInfo(undefined) before I
fetch a new Pokemon from the API.
I think sometimes, this manner of fetching data inside a component can lead to
what some web-perf-experts refer to as waterfall. Can you get your state from
your parent? That might result in fewer individual requests made, but is also
quite a different architecture.
#Footnote 1 - ErrorBoundaries don't automatically save you from manually handling error
You can also consider using an ErrorBoundary, but this does not automatically
catch errors that happen in e.g. a useEffect. If you want your ErrorBoundary to
handle your useEffect related error, then you can use something like this. This
assumes a react-error-boundary type ErrorBoundary.
Another trick, instead of throwing in the body of the component is throwing in
the callback form of the useState-setter. Then you wouldn't necessarily need to
have a separate useState for the error state, but you would then need an
ErrorBoundary or something to help display a nice error.
These libraries definitely do a lot of things, so take on some more baggage
than the simple hooks described above, but may be helpful to you also.
#Footnote 4: Fetching is just one aspect of this blogpost
Really, the thing I wanted to make more clear in general was also how "sticky"
useState can be. I find other patterns in my codebase besides just fetching
where I have to "reset" the useState hook to a neutral state, sometimes related
to controlled components.
#Footnote 5: You can also use the "key" prop as an alternative to manually resetting state
I am not sure I recommend this as it basically forces the component to unmount,
which may be ok in some cases but I don't know all the ramifications. A quote
from https://kentcdodds.com/blog/understanding-reacts-key-prop explains
"This allows you to return the exact same element type, but force React to
unmount the previous instance, and mount a new one. This means that all state
that had existed in the component at the time is completely removed and the
component is "reinitialized" for all intents and purposes."