Think of the following React component:
const [page, setPage] = useState(1)
const [search, setSearch] = useState("")
const [data, setData] = useState([])
const fetchData = useCallback(() => {
get({page: page, search: search}.then(setData)
}, [page, search])
useEffect(() => {
fetchData()
}, [page]) // <----- here I got the react-hooks/exhaustive-deps lint error
// if I add `fetchData` above, `fetchData` will be called whenever `search` change,
// but I only want to fetch data when `page` change.
return <div>
<Input value={search} setValue={setSearch}/>
<Button onClick={() => fetchData()}>Search</Button>
{data.maps(v => <div>{v}</div>}
<Select value={page} setValue={setPage}/>
</div>
The goal is really simple:
- fetch data when page change;
- do not fetch data when search change;
- fetch data when the button is clicked;
I really cannot think of any other way to bypass the eslint rule. Thanks for any help.
3
Answers
This is one of the most common objections that I can think of to any react linting rule, and it’s somewhat warranted- our mental model for how dependencies work is that they are all ‘reactive’, and that when they change, we necessarily want to re-run all of the logic inside of the effect.
In this case, the
search
dependency falls under the second bullet point under ‘Removing Unnecessary Dependencies’ in the React docs, where we just want to read its latest value:You could read the current reference for page, and only run your effect when the page changes.
However, this leaves us with a very odd user experience- you said, "do not fetch data when search changes". Does this mean that we only ever return search results, for example a text string typed into an input field, after the user has clicked to change the page?
This implementation seems counter-intuitive to how a search field should work- i.e., you receive results back for the search query immediately, not after taking some subsequent action (changing the page).
I would imagine that, in practice, there actually is a reactive relationship between
search
andgetData()
. It’s hard to imagine decoupling the method from the search dependency.In general disabling exhaustive dependencies using eslint-disable is not a great idea longterm for the health of a useEffect. Especially if more methods and data are tacked on.
For this example usecase, everytime the page changes there is an interaction between the useEffect and the callback.
The callback is the only real necessary dependency to trigger the hook since it already updates when page and search change.
Ignoring this interaction could lead to memory leaks and other weird async setState issues.
Example
What happens if the callback is delayed but the page changes?
Technically this is a memory leak (in the example above). If you fetch multiple pages it will fire setState Asynchronously, per call that resolves instead of making sure it’s not unmounted or if it’s on the correct page.
Recommendation:
search
is the correct dep, and you should keep it there. However, if you only mean for the "actual" value for the data request to change only after the user accepts it with theSearch
button, simply have a separate state atom for it:However, you probably shouldn’t be doing data fetching with
useEffect
by hand at all, since as it is, you’re missing e.g. error states, loading states, etc.The lightweight option is swr, and you’d get something like: