9

I have a use-case where a page have to call the same fetch function on first render and on button click.

The code is similar to the below (ref: https://stackblitz.com/edit/stackoverflow-question-bink-62951987?file=index.tsx):

import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { fetchBackend } from './fetchBackend';

const App: FunctionComponent = () => {
  const [selected, setSelected] = useState<string>('a');
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<boolean>(false);
  const [data, setData] = useState<string | undefined>(undefined);

  const query = useCallback(async () => {
    setLoading(true)

    try {
      const res = await fetchBackend(selected);
      setData(res);
      setError(false);
    } catch (e) {
      setError(true);
    } finally {
      setLoading(false);
    }
  }, [])

  useEffect(() => {
    query();
  }, [query])

  return (
    <div>
      <select onChange={e => setSelected(e.target.value)} value={selected}>
        <option value="a">a</option>
        <option value="b">b</option>
      </select>
      <div>
        <button onClick={query}>Query</button>
      </div>
      <br />
      {loading ? <div>Loading</div> : <div>{data}</div>}
      {error && <div>Error</div>}
    </div>
  )
}

export default App;

The problem for me is the fetch function always triggers on any input changed because eslint-plugin-react-hooks forces me to declare all dependencies (ex: selected state) in the useCallback hook. And I have to use useCallback in order to use it with useEffect.

I am aware that I can put the function outside of the component and passes all the arguments (props, setLoading, setError, ..etc.) in order for this to work but I wonder whether it is possible to archive the same effect while keeping the fetch function inside the component and comply to eslint-plugin-react-hooks?


[UPDATED] For anyone who is interested in viewing the working example. Here is the updated code derived from the accepted answer. https://stackblitz.com/edit/stackoverflow-question-bink-62951987-vxqtwm?file=index.tsx

binkpitch
  • 677
  • 1
  • 8
  • 18
  • How do you protect against memory leaks with this approach? If a user navigates away from the page before the response comes back, you've not protected yourself. – BryceBy Feb 17 '22 at 20:13

3 Answers3

4

Add all of your dependecies to useCallback as usual, but don't make another function in useEffect:

useEffect(query, [])

For async callbacks (like query in your case), you'll need to use the old-styled promise way with .then, .catch and .finally callbacks in order to have a void function passed to useCallback, which is required by useEffect.

Another approach can be found on React's docs, but it's not recommended according to the docs.

After all, inline functions passed to useEffect are re-declared on each re-render anyways. With the first approach, you'll be passing new function only when the deps of query change. The warnings should go away, too. ;)

Rosen Dimov
  • 1,055
  • 12
  • 32
  • Thank you for showing me the possible approaches. The `useEffect(query, [])` looks great to me but when I tried to use it with my code, the typescript gives me an error (see https://imgur.com/a/tP0Lvp0.) because the `useEffect` does not accept the type '() => Promise'. – binkpitch Jul 17 '20 at 10:50
  • 1
    Yeah, I didn't notice that it's async. In this case you can avoid that error if you use the old-style approach with ```.then```, ```.catch``` and ```.finally``` on the promise. With that, you won't be returning a promise, anymore. – Rosen Dimov Jul 17 '20 at 11:00
  • I tried the old-style approach at https://stackblitz.com/edit/stackoverflow-question-bink-vxqtwm. But, when the select component changed, it still triggers re-render. Did I miss anything? – binkpitch Jul 17 '20 at 11:30
  • You haven't replaced the effect with ```useEffect(query, [])``` You also no longer need the ```async``` keyword in front of query function. – Rosen Dimov Jul 17 '20 at 11:39
  • Sorry, my mistake. I have updated the code and it worked beautifully. Thank you so much! – binkpitch Jul 17 '20 at 11:52
  • How do you protect against memory leaks with this approach? If a user navigates away from the page before the response comes back, you've not protected yourself. – BryceBy Feb 17 '22 at 20:13
1

There are a few models to achieve something where you need to call a fetch function when a component mounts and on a click on a button/other. Here I bring to you another model where you achieve both by using hooks only and without calling the fetch function directly based on a button click. It'll also help you to satisfy eslint rules for hook deps array and be safe about infinite loop easily. Actually, this will leverage the power of effect hook called useEffect and other being useState. But in case you have multiple functions to fetch different data, then you can consider many options, like useReducer approach. Well, look at this project where I tried to achieve something similar to what you wanted.

https://codesandbox.io/s/fetch-data-in-react-hooks-23q1k?file=/src/App.js

Let's talk about the model a bit

export default function App() {
  const [data, setDate] = React.useState("");
  const [id, setId] = React.useState(1);
  const [url, setUrl] = React.useState(
    `https://jsonplaceholder.typicode.com/todos/${id}`
  );
  const [isLoading, setIsLoading] = React.useState(false);

  React.useEffect(() => {
    fetch(url)
      .then(response => response.json())
      .then(json => {
        setDate(json);
        setIsLoading(false);
      });
  }, [url]);

  return (
    <div className="App">
      <h1>Fetch data from API in React Hooks</h1>
      <input value={id} type="number" onChange={e => setId(e.target.value)} />
      <button
        onClick={() => {
          setIsLoading(true);
          setUrl(`https://jsonplaceholder.typicode.com/todos/${id}`);
        }}
      >
        GO & FETCH
      </button>
      {isLoading ? (
        <p>Loading</p>
      ) : (
        <pre>
          <code>{JSON.stringify(data, null, 2)}</code>
        </pre>
      )}
    </div>
  );
}

Here I fetched data in first rendering using the initial link, and on each button click instead of calling any method I updated a state that exists in the deps array of effect hook, useEffect, so that useEffect runs again.

Mateen
  • 1,455
  • 10
  • 17
0

I think you can achieve the desired behavior easily as

useEffect(() => {
    query();
  }, [data]) // Only re-run the effect if data changes

For details, navigate to the end of this official docs page.

DevLoverUmar
  • 11,809
  • 11
  • 68
  • 98
  • Thank for your response. I have tried it and I think this approach cause the fetch function to be called twice. The first time when data is null (not fetched yet) and the second time the when data exists. – binkpitch Jul 17 '20 at 11:07
  • The eslint also throw an error `React Hook useEffect has a missing dependency: 'query'. Either include it or remove the dependency array.eslint(react-hooks/exhaustive-deps)`. – binkpitch Jul 17 '20 at 11:08