Big disclaimer (please read before the rest of the post)
Looking back at this post, many of the “lessons” below are not really that valid. The most important thing is to first ask whether you need an effect at all. Please read You Might Not Need an Effect from the React docs. It is genuinely required reading.
Most of the patterns in this post (manual
useStateresets,cancelledflags, throwing inside setter callbacks, etc.) are workarounds for problems you create by reaching foruseEffectin the first place. In almost every case you are better off:
- Using the
keyprop to reset component state when an id changes (see Footnote 5), instead of manually resetting state inside an effect- Using a data-fetching library like
swr(see Footnote 3), which handles caching, cancellation, and state resets for you- Lifting fetching out of the component entirely (server components, route loaders, parent fetching)
I’m leaving the post up because the “sticky
useState” point is still occasionally useful, but treat the code below as a cautionary example of what happens when you commit touseEffectdata fetching, not as a pattern to copy.
If you make a React component with a prop for an item id and a useEffect to
fetch data for that item, you probably also have a useState for the result or
error. The kernel of truth this post is built around is:
useState does not reset when props change.
The 2022 version of this post said “remember to manually call
setError(undefined) and setData(undefined) at the top of your effect.” That
works, but it is a symptom of an architecture that does not compose. The
recommended fixes are above in the disclaimer. The original walkthrough follows
below as a cautionary example.
Part 1: Having component state for API response or error (the cautionary version)
Working codesandbox
https://codesandbox.io/s/practical-rubin-l2d5el?file=/src/App.tsx:0-2003
When refetching a new item, you need to clear the previous state, otherwise
you’ll show stale results while the new fetch is in flight. The key prop
gives you this for free (see Footnote 5); doing it by hand looks like:
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 >
)
}
Part 2: A custom hook?
You can encapsulate the related hooks (useState for error and data, plus
useEffect) into a single custom hook. The approach is the same, but callers
just write usePokemonInfo(pokemonName) and get error handling and fetching for
free. At this point you are effectively reinventing a worse version of swr,
which is one reason the disclaimer recommends just using swr instead.
Working codesandbox
https://codesandbox.io/s/fragrant-wind-008pfn?file=/src/App.tsx:0-2234
import { useState , useEffect } from 'react'
// ...same myfetch, PokemonType, PokemonInfo, ErrorMessage as above...
function usePokemonInfo ( pokemonName : string ) {
const [ error , setError ] = useState < unknown >()
const [ pokemonInfo , setPokemonInfo ] = useState < PokemonInfo >()
useEffect (() => {
let cancelled = false
;( async () => {
try {
setPokemonInfo ( 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 ) {
setPokemonInfo ( data )
}
} catch ( e ) {
console . error ( e )
if ( ! cancelled ) {
setError ( e )
}
}
})()
return () => {
cancelled = true
}
}, [ pokemonName ])
return [ error , pokemonInfo ] as const
}
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 >
)
}
Conclusion
The original takeaway here was “useState is sticky, so remember to reset it”
along with “remember to handle errors, there are no lint rules for it.” Both
points are still true in a narrow sense, but the better takeaway is: don’t put
yourself in a position where you need to remember any of that. Use the key
prop, use swr, or move fetching out of the component, and the manual reset
and error plumbing both disappear.
If you do end up writing the manual version, note that it is easy to forget
error handling entirely in async useEffect code, since there are no lint
rules to catch it. The user just sees a stuck “Loading…” while the error sits
in the console.
Footnote 0 - Web perf pontificating
Fetching inside a component can lead to what web-perf folks call waterfall. Lifting the fetch to a parent might reduce individual requests, but it’s quite a different architecture.
Footnote 1 - ErrorBoundaries don’t automatically save you from manually handling error
ErrorBoundary does not automatically catch errors from useEffect. To route a
useEffect error through an ErrorBoundary (e.g. react-error-boundary), you
can throw it in the component body:
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 is throwing inside the useState setter callback, which lets you
drop the separate error state entirely — though you’d still need an
ErrorBoundary to display it nicely.
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 ])
Footnote 2: The future with React data fetching
See https://github.com/reactjs/rfcs/pull/229
Footnote 3: Using swr
swr demo:
https://codesandbox.io/s/condescending-poitras-fiwxym?file=/src/App.tsx
It does a lot more than the simple hooks above, so it carries more baggage, but for most use cases the tradeoff is worth it.
Footnote 4: Fetching is just one aspect of this blogpost
The broader point is how “sticky” useState can be. I run into this beyond just
fetching — anytime I have a controlled component that needs to reset when its
props change.
See also https://bikeshedd.ing/posts/use_state_should_require_a_dependency_array/
Footnote 5: You can also use the “key” prop as an alternative to manually resetting state
See https://codesandbox.io/s/cool-grass-9nb43y?file=/src/App.tsx
This forces the component to unmount and remount, wiping all state. A quote from https://kentcdodds.com/blog/understanding-reacts-key-prop explains it well:
“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.”
I’m not sure I’d recommend it by default — unmounting has side effects — but it’s useful to know about.
See also https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes
Footnote 6: The use hook
https://react.dev/reference/react/use
use is a stable React API that reads a Promise or context value. Its main
data-fetching use case is with Server Components: the server creates a Promise,
passes it as a prop, and the client component calls use(promise), which
suspends until it resolves (integrated with Suspense and ErrorBoundary). It’s
not really a replacement for the useEffect pattern shown in this post — if
you’re doing pure client-side fetching without Server Components, use doesn’t
help much here.
Footnote 7: AbortController vs the cancelled flag
The examples use a cancelled boolean rather than AbortController.
AbortController is more correct — it actually cancels the in-flight request —
but threading an AbortSignal through every layer of your call chain,
especially when the fetch is buried several functions deep, adds real complexity
and is easy to get wrong. The cancelled flag is simpler, covers the part that
matters (no stale renders), and is easier to reason about. If cancelling the
request itself matters for your use case (e.g. reducing server load),
AbortController is worth the extra work.