Skip first useEffect when there are multiple useEffects
Asked Answered
T

3

8

To restrict useEffect from running on the first render we can do:

  const isFirstRun = useRef(true);
  useEffect (() => {
    if (isFirstRun.current) {
      isFirstRun.current = false;
      return;
    }

    console.log("Effect was run");
  });

According to example here: https://mcmap.net/q/1328528/-react-hooks-skip-first-run-in-useeffect-duplicate

But what if my component has multiple useEffects, each of which handle a different useState change? I've tried using the isFirstRun.current logic in the other useEffect but since one returns, the other one still runs on the initial render.

Some context:

const Comp = () => {
const [ amount, setAmount ] = useState(props.Item ? Item.Val : 0);
const [ type, setType ] = useState(props.Item ? Item.Type : "Type1");

useEffect(() => {
    props.OnAmountChange(amount);
}, [amount]);

useEffect(() => {
    props.OnTypeChange(type);
}, [type]);

return {
    <>
        // Radio button group for selecting Type
        // Input field for setting Amount
    </>
}
}

The reason I've used separate useEffects for each is because if I do the following, it doesn't update the amount.

useEffect(() => {
    if (amount) {
        props.OnAmountChange(amount);
    } else if (type) {
        props.OnTypeChange(type)
    }
}, [amount, type]);
Thud answered 28/7, 2019 at 10:54 Comment(8)
Please show a complete exampleTelpher
If the requirement is to run multiple effects after the first render, why not put multiple effects in that single useEffect call?Irisirisa
is the question in context of reactjs(how to implement that) or jest(how to unit test that)?Anemometry
could you provide more details on why do you need such a logic? to me it'd easier to decompose component into 2 with different responsibilities.Anemometry
@Anemometry More on the React front on how to implement itThud
@Anemometry I've updated the original post with some more context.Thud
How this code even compiles... fix itTrichomoniasis
Haven't tested or anything but could you not just set the ref to true in the last useEffect?Rockrose
T
9

As far as I understand, you need to control the execution of useEffect logic on the first mount and consecutive rerenders. You want to skip the first useEffect. Effects run after the render of the components.

So if you are using this solution:

const isFirstRun = useRef(true);
  useEffect (() => {
    if (isFirstRun.current) {
      isFirstRun.current = false;
      return;
    }

    console.log("Effect was run");
  });
   useEffect (() => {
    // second useEffect
    if(!isFirstRun) {
        console.log("Effect was run");
     }
   
  });

So in this case, once isFirstRun ref is set to false, for all the consecutive effects the value of isFirstRun becomes false and hence all will run.

What you can do is, use something like a useMount custom Hook which can tell you whether it is the first render or a consecutive rerender. Here is the example code:

const {useState} = React

function useMounted() {
  const [isMounted, setIsMounted] = useState(false)


  React.useEffect(() => {
    setIsMounted(true)
  }, [])
  return isMounted
}

function App() {


  const [valueFirst, setValueFirst] = useState(0)
  const [valueSecond, setValueSecond] = useState(0)

  const isMounted = useMounted()

  //1st effect which should run whenever valueFirst change except
  //first time
  React.useEffect(() => {
    if (isMounted) {
      console.log("valueFirst ran")
    }

  }, [valueFirst])


  //2nd effect which should run whenever valueFirst change except
  //first time
  React.useEffect(() => {
    if (isMounted) {
      console.log("valueSecond ran")
    }

  }, [valueSecond])

  return ( <
    div >
    <
    span > {
      valueFirst
    } < /span> <
    button onClick = {
      () => {
        setValueFirst((c) => c + 1)
      }
    } >
    Trigger valueFirstEffect < /button> <
    span > {
      valueSecond
    } < /span> <
    button onClick = {
      () => {
        setValueSecond((c) => c + 1)
      }
    } >
    Trigger valueSecondEffect < /button>

    <
    /div>
  )
}

ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

I hope it helps !!

Tobolsk answered 28/7, 2019 at 16:1 Comment(2)
Perfect. This is exactly what I was looking for, thank you.Thud
didnt work for me, cant see how it works, my ref doesnt get updated with new value by the time second useEffect runsBestride
I
0

You can use a single useEffect to do both effects in, you just implemented the logic incorrectly.

Your original attempt:

useEffect(() => {
  if (amount) {
      props.OnAmountChange(amount);
  } else if (type) {
      props.OnTypeChange(type)
  }
}, [amount, type]);

The issue here is the if/elseif, treat these as independent effects instead:

useEffect(() => {
  if (amount !== 0) props.onAmountChange(amount);
  if (type !== "Type1") props.onTypeChange(type);
}, [amount, type])

In this method if the value is different than the original value, it will call the on change. This has a bug however in that if the user ever switches the value back to the default it won't work. So I would suggest implementing the entire bit of code like this instead:

const Comp = () => {
  const [ amount, setAmount ] = useState(null);
  const [ type, setType ] = useState(null);

  useEffect(() => {
    if (amount !== null) {
      props.onAmountChange(amount);
    } else {
      props.onAmountChange(0);
    }
  }, [amount]);

  useEffect(() => {
    if (type !== null) {
      props.onTypeChange(type);
    } else {
      props.onTypeChange("Type1");
    }
  }, [type]);

  return (
    <>
        // Radio button group for selecting Type
        // Input field for setting Amount
    </>
  )
}

By using null as the initial state, you can delay calling the props methods until the user sets a value in the Radio that changes the states.

Irisirisa answered 28/7, 2019 at 11:59 Comment(0)
A
0

If you are using multiple useEffects that check for isFirstRun, make sure only the last one (on bottom) is setting isFirstRun to false. React goes through useEffects in order!

creds to @Dror Bar comment from react-hooks: skip first run in useEffect

Allianora answered 30/11, 2022 at 7:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.