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
Working codesandbox
https://codesandbox.io/s/practical-rubin-l2d5el?file=/src/App.tsx:0-2003
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)
import { useState, useEffect } from 'react'
interface PokemonType {
type: {
name: string
}
}
interface PokemonInfo {
name: string
types: PokemonType
}
// util fetch function to throw if !response.ok, I use this util often
async function myfetch(url: string, opts?: RequestInit) {
const response = await fetch(url, opts)
if (!response.ok) {
throw new Error(
`Error fetching ${url}: HTTP ${response.status} ${await response.text()}`,
)
}
return response.json()
}
function ErrorMessage({ error }: { error: unknown }) {
return <div style={{ background: 'red' }}>{`${error}`}</div>
}
function PokemonCard({ pokemonName }: { pokemonName: string }) {
const [error, setError] = useState<unknown>()
const [pokemonInfo, setPokemonInfo] = useState<PokemonInfo>()
useEffect(() => {
let cancelled = false
;(async () => {
try {
// important: reset the error and item state of the component!
setError(undefined)
setPokemonInfo(undefined)
const data = await myfetch(
`https://pokeapi.co/api/v2/pokemon/${pokemonName}`,
)
if (!cancelled) {
setPokemonInfo(data)
}
} catch (e) {
console.error(e)
if (!cancelled) {
setError(e)
}
}
})()
return () => {
cancelled = true
}
}, [pokemonName])
return (
<div>
{error ? (
<ErrorMessage error={error} />
) : pokemonInfo ? (
<div>
{pokemonInfo.name} is of type{' '}
{pokemonInfo.types.map(t => t.type.name).join(', ')}
</div>
) : (
<div>Loading...</div>
)}
</div>
)
}
export default function App() {
const [value, setValue] = useState('oddish')
return (
<div className="App">
<label htmlFor="pokemon_name">Pokemon name</label>
<input
id="pokemon_name"
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
<PokemonCard pokemonName={value} />
</div>
)
}
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
Working codesandbox
https://codesandbox.io/s/fragrant-wind-008pfn?file=/src/App.tsx:0-2234
import { useState, useEffect } from 'react'
interface PokemonType {
type: {
name: string
}
}
interface PokemonInfo {
name: string
types: PokemonType[]
}
// util fetch function to throw if !response.ok, I use this util often
async function myfetch(url: string, opts?: RequestInit) {
const response = await fetch(url, opts)
if (!response.ok) {
throw new Error(
`Error fetching ${url}: HTTP ${response.status} ${await response.text()}`,
)
}
return response.json()
}
function usePokemonInfo(pokemonName: string) {
const [error, setError] = useState<unknown>()
const [pokemonInfo, setItemInfo] = useState<PokemonInfo>()
useEffect(() => {
let cancelled = false
;(async () => {
try {
setItemInfo(undefined) // <-- important to reset the state of the app
setError(undefined) // <-- important to reset the state of the app
const data = await myfetch(
`https://pokeapi.co/api/v2/pokemon/${pokemonName}`,
)
if (!cancelled) {
setItemInfo(data)
}
} catch (e) {
console.error(e)
if (!cancelled) {
setError(e)
}
}
})()
return () => {
cancelled = true
}
}, [pokemonName])
return [error, pokemonInfo] as const
}
function ErrorMessage({ error }: { error: unknown }) {
return <div style={{ background: 'red' }}>{`${error}`}</div>
}
function PokemonCard({ pokemonName }: { pokemonName: string }) {
const [error, pokemonInfo] = usePokemonInfo(pokemonName)
return (
<div>
{error ? (
<ErrorMessage error={error} />
) : pokemonInfo ? (
<div>
{pokemonInfo.name} is of type{' '}
{pokemonInfo.types.map(t => t.type.name).join(', ')}
</div>
) : (
<div>Loading...</div>
)}
</div>
)
}
export default function App() {
const [value, setValue] = useState('oddish')
return (
<div className="App">
<label htmlFor="pokemon_name">Pokemon name</label>
<input
id="pokemon_name"
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
<PokemonCard pokemonName={value} />
</div>
)
}
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.
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.
import { ErrorBoundary } from 'react-error-boundary'
function PokemonCard({ pokemonName }: { pokemonName: string }) {
const [error, pokemonInfo] = usePokemonInfo(pokemonName)
if (error) {
throw error
}
return (
<div>
{pokemonInfo ? (
<div>
{pokemonInfo.name} is of type{' '}
{pokemonInfo.types.map(t => t.type.name).join(', ')}
</div>
) : (
<div>Loading...</div>
)}
</div>
)
}
export default function App() {
const [value, setValue] = useState('oddish')
return (
<ErrorBoundary FallbackComponent={({ error }) => <div>{`${error}`}</div>}>
<PokemonCard pokemonName={value} />
</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.
useEffect(() => {
let cancelled = false
;(async () => {
try {
// important: reset the error and item state of the component!
setPokemonInfo(undefined)
const data = await myfetch(
`https://pokeapi.co/api/v2/pokemon/${pokemonName}`,
)
if (!cancelled) {
setPokemonInfo(data)
}
} catch (e) {
console.error(e)
if (!cancelled) {
setPokemonInfo(() => {
throw e
})
}
}
})()
return () => {
cancelled = true
}
}, [pokemonName])
See https://github.com/reactjs/rfcs/pull/229
This was just announced so there is a lot to unpack there, I can update this blog post if I come up with an analogous example using this RFC
There are helper libraries that try to help
One helper library suggested was called react-query
, so I made a demo using
@tanstack/react-query
v4.
https://codesandbox.io/s/hungry-framework-ctmhkz?file=/src/App.tsx
Another is swr
, here is a demo for that library
https://codesandbox.io/s/condescending-poitras-fiwxym?file=/src/App.tsx
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.
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.
See https://codesandbox.io/s/cool-grass-9nb43y?file=/src/App.tsx
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."
See also https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes
use
hookMight also be related https://blixtdev.com/all-about-reacts-new-use-hook/