7

I know why this happens but want to know what´s the right approach to avoid it. useEffect is called twice. one with data = null and then with "real" data.

const [data, setData] = useState(null);
someServiceToFetchData.then(freshData => setData(freshData))

useEffect(() => {
    console.log("useEffect called");
}, [data]);

should I do something like this everywhere?

useEffect(() => {
    if (data) {
        console.log("useEffect called with real data");
    }
}, [data]);
handsome
  • 2,335
  • 7
  • 45
  • 73
  • `useEffect` is called once per render, is there a specific issue with that? If you just don't want to see two console logs I wouldn't worry about it and the former is ok, if the effect is doing a meaningful side-effect with `data` and it needs to be defined, then the latter. – Drew Reese Apr 06 '20 at 21:28
  • Those aren't functionally equivalent. It depends on when your effect needs to trigger. – Patrick Roberts Apr 06 '20 at 21:29
  • well. it triggers on every data change and data first is null and then... it has some data. will update the example to make it more clear. – handsome Apr 06 '20 at 21:31
  • I mean, you're welcome to update the question, but my response will still be the same. Neither of those is "right" and the other "wrong", they're not functionally equivalent and you need to choose one depending on what logic you need. – Patrick Roberts Apr 06 '20 at 21:32
  • `someServiceToFetchData` sounds very effect-y to me. This should likely be within this or another effect depending on your requirements. – Yury Tarabanko Apr 06 '20 at 21:35
  • If what you do in the effect matters if `data` is null/undefined or not, then you should correctly guard against "access of undefined..." errors, so yeah, the second one I guess. – Drew Reese Apr 06 '20 at 21:35
  • The first call was when the component was mounted, the second call was when the `data` state was updated – jstarnate Apr 06 '20 at 22:05
  • If the dependency array is empty and `useEffect` is called twice, see https://stackoverflow.com/questions/60618844/react-hooks-useeffect-is-called-twice-even-if-an-empty-array-is-used-as-an-ar – Ben Butterworth Jun 28 '22 at 15:22
  • For those who are facing this with the new React version, visit https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react/72238236#72238236 – Youssouf Oumar Dec 16 '22 at 14:52

2 Answers2

5

If you want to skip the first render you could use a state flag like this. I find the method below less complicated.

import React, { useState } from 'react';

const Comp = ({dep}) => {
    const [rendered, setRendered] = useState(false);

    useEffect(() => {
        if(rendered){
            // do stuff
        }
        
        if( ! rendered ) {
            setRendered(true);
        }
    }, [dep]);

    // rest of the component code

    return <></>
}
Sisir
  • 2,668
  • 6
  • 47
  • 82
  • For those who are facing this with the new React version, visit https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react/72238236#72238236 – Youssouf Oumar Dec 16 '22 at 14:52
3

As we all know the useEffect is called once on initial render and also on subsequent change of values of dependency array.

In your case, to skip initial execution of useEffect (null case), write a little custom hook like which can be used generically when needed.

Component

import React,  { useState, useEffect } from 'react';
import useEffectSkipInitialRender from "./hook";

const Test = (props) => {
    const [data, setData] = useState(null);

    new Promise((res, rej) => res('my data')).then(freshData => setData(freshData));

    useEffectSkipInitialRender(() => {
        console.log("useEffect called");
    }, [data]);

    return <div>hi</div>
};

export default Test;

Custom hook

import React,  { useState, useEffect, useRef } from 'react';

const useEffectSkipInitialRender = (callback, dataArr) => {
    const [data, setData] = useState(null);
    const isInitialRender = useRef(true);// in react, when refs are changed component dont re-render 

    useEffect(() => {
        if(isInitialRender.current){// skip initial execution of useEffect
            isInitialRender.current = false;// set it to false so subsequent changes of dependency arr will make useEffect to execute
            return;
        }
        return callback();
    }, dataArr);

};

export default useEffectSkipInitialRender;

If you want effect logic to run only when data is not null and data changes, you can write your custom hook like this:

import React,  { useEffect } from 'react';

const useEffectOnDataChange = (callback, dataArr) => {
    const someIsNull = dataArr.some(data => data == null);

    useEffect(() => {
        if (someIsNull) return;
        return callback();
    }, dataArr);

}
hackape
  • 18,643
  • 2
  • 29
  • 57
gdh
  • 13,114
  • 2
  • 16
  • 28
  • Nice. But I think the PO's point is to skip when `data == null` instead of initial render – hackape Apr 07 '20 at 04:39
  • For those who are facing this with the new React version, visit https://stackoverflow.com/questions/72238175/why-useeffect-running-twice-and-how-to-handle-it-well-in-react/72238236#72238236 – Youssouf Oumar Dec 16 '22 at 14:53