31

Can I call another separate function in useEffect?

I am calling another function in useEffect but after saving the file it is automatically adding that function in array parameters of useEffect.

See the code below for proper understanding.

Before saving file:

useEffect(() => {
  getData()
  console.log("useEffect ran...");
}, [query]);

function getData() {
  fetch(`https://jsonplaceholder.typicode.com/${query}`)
  .then(response => response.json())
  .then(json => setData(json));
}

after saving file:

useEffect(() => {
  getData();
  console.log("useEffect ran...");
}, [getData, query]);

function getData() {
  fetch(`https://jsonplaceholder.typicode.com/${query}`)
    .then(response => response.json())
    .then(json => setData(json));
}

it is running again and again.

Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
Waseem Maya
  • 433
  • 1
  • 5
  • 7
  • 1
    React docs on the `useEffect` hook mention this because the hook as you wrote it will fire on every render. The function inside causes re-render and boom, there's your loop. There are ways to check if certain props have changed and conditionalize your function execution inside the hook based on that. https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects – BugsArePeopleToo Jun 01 '19 at 21:05

6 Answers6

63

TL;DR use useCallback()


First of all, using a function as a dependency is very dangerous. If that function causes a state change, then the subsequent re-render will invoke the function again (through useEffect)... and an infinite loop begins.

One thing you can do, as many suggest here, is to create the function within the useEffect() method itself.

 useEffect(() => {
    function getData() {
      fetch(`https://jsonplaceholder.typicode.com/${query}`)
        .then(response => response.json())
        .then(json => setData(json));
      }
    }

    getData();
    console.log("useEffect ran...");
  }, [query]);
}

or simply

 useEffect(() => {
    (() => {
       fetch(`https://jsonplaceholder.typicode.com/${query}`)
      .then(response => response.json())
      .then(json => setData(json)
     )();
  }, [query]);
}

Having said that, sometimes you want to still declare the function outside the useEffect for code-reuse. This time you need to make sure it is not re-created during each render. For that, you can either

  1. declare the function outside the component -- which forces you to pass all variables as arguments.... which is a pain

  2. wrap it inside useMemo() or useCallback()

Here's an example

    const getData = useCallback(()=>{...}, []);
DanielK
  • 792
  • 1
  • 6
  • 13
  • 16
    Hey! Your answer helped me to understand useCallback for the fist time! I could solve my problem using your answer as example, thank you very much! – Fabio Sousa Jul 02 '20 at 02:22
  • 1
    I have used useCallback but it still complains about the function as a dependency. In my case I have an async function that loads a font into opentype.js const loadFont = useCallback(async ()=>{...}, []); and it wants me to put loadFont into dep array. If I do that the function always gets called twice. – user6329530 Apr 23 '21 at 16:31
  • @user6329530.Try this. https://stackoverflow.com/questions/64836671/call-function-from-useeffect-and-other-function – quine9997 Nov 15 '21 at 14:32
  • We can use `useRef` for the same. [stackoverflow.com/a/70255826/8494462](https://stackoverflow.com/a/70255826/8494462) – Shivam Sharma Dec 07 '21 at 06:39
28

Since you declare the getData function inside your React component it will be re-created on each render and thus the dependencies of your effect change on each render. This is the reason why the effect is executed on each render.

To prevent this you should declare the getData function outside of the component and then pass query. Like so:

function getData(query) {
  return fetch(`https://jsonplaceholder.typicode.com/${query}`)
    .then(response => response.json());
}

function YouComponent({ query }) {
  ...
  useEffect(() => {
    getData(query).then(setData);
    console.log("useEffect ran...");
  }, [query]);

  ...

P.S: I'm not sure whether the eslint plugin will add getData to the dependencies automatically when doing it like this but even if it does so it doesn't hurt.

Sven Tschui
  • 1,377
  • 8
  • 20
  • Does it make a difference if you define the getData function inside useEffect or outside, and just call it from within useEffect? – timman Apr 17 '21 at 08:53
  • Depends on what "outside" means. You must not define it inside `YourComponent` function body as it would change on every render and trigger the useEffect on each render. You can define it inside `useEffect`s callback though, this is completely fine. See the answer by @DanielK for further details – Sven Tschui Jun 04 '21 at 11:26
  • in this case where you have `getData` not listed as a dependency, can it not just be inside the component? it would re-create every-time, but there will be no loop as there is no dependency on `getData`. is this correct? – user566245 Apr 10 '22 at 19:55
8

The accepted answer is misleading in some way. Defining function inside component obviously re-creates on each render. But it does not mean that it will re-call in each render when using inside useEffect hook.

The key problem in your after saving file code, that you are watching for getData. As it is re-creating on each render, the useEffect accepts it as a change and thus cause you re-run on each render. The simple fix is not to watch getData.

But obviously, as suggested in the accepted answer. It's more good practice to separate the function outside the component so it won't re-create on each render.

Frankly, if I were you, I would not define function just for fetch:

useEffect(() => {
  fetch(`https://jsonplaceholder.typicode.com/${query}`)
    .then(response => response.json())
    .then(json => setData(json));
}, [query]); // only re-run on query change
Bhojendra Rauniyar
  • 83,432
  • 35
  • 168
  • 231
  • 2
    The original question explicitly states that the dependency is added automatically when saving the file, I assume eslint-plugin-react-hooks is causing this. IMO it is an anti-pattern to by-design omit dependencies inside useEffect as it can lead to a lot of headache when something starts to go wrong once these dependencies start to add dependencies to outer closures. – Sven Tschui Jun 04 '21 at 11:30
2

You can also use useRef to create a ref and put the function definition there and call it using ref.current(). Even React supports using ref as instance variables.

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.

useEffect(() => {
  getData.current()
  console.log("useEffect ran...");
}, [query]);

const getData = useRef(() {
  fetch(`https://jsonplaceholder.typicode.com/${query}`)
  .then(response => response.json())
  .then(json => setData(json));
});

Important:

If you want to use any state variable inside this method then you'll have to create a mutable object (like another ref) that will contain the latest value of state as if you directly refer to a state variable in that then that will be referring to the old/default value which was created at the time of method creation. To know more about that you can refer here. The same is true for useCallback approach as well.

Shivam Sharma
  • 1,277
  • 15
  • 31
  • It should be useRef(()=>{ fetch(`https://jsonplaceholder.typicode.com/${query}`) .then(response => response.json()) .then(json => setData(json))}) – passion Oct 10 '22 at 11:09
1

If you move your getData function inside useEffect, you won’t have to include it as a dependency, and useEffect will only run when query changes.

helloitsjoe
  • 6,264
  • 3
  • 19
  • 32
  • 1
    Declaring the `getData` function outside of the React component and passing the query into the component will save a bunch of function creations - especially if `query` changes a lot – Sven Tschui Jun 01 '19 at 21:16
  • 3
    but, what if you need use getData function in more places?, won't be a good idea move inside of useEffect, because you will have that function duplicated – CrsCaballero Nov 18 '19 at 21:12
0

Try wrapping the getData function inside useCallback hook and put it outside the component.So when getData will be called then it will not return a new function all the time and hence it will prevent the component from re renders

  • 1
    Hooks can only be called from within components, so moving the functon outside the component and wrapping in useCallback cannot happen at the same time. – Daniel Nora Jan 03 '23 at 22:27