blankface
blankface

Reputation: 6347

Skip first useEffect when there are multiple useEffects

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://stackoverflow.com/a/53351556/3102993

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]);

Upvotes: 8

Views: 15729

Answers (3)

Louis Nguyen
Louis Nguyen

Reputation: 49

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

Upvotes: 0

simbathesailor
simbathesailor

Reputation: 3687

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 !!

Upvotes: 9

Ryan Castner
Ryan Castner

Reputation: 1054

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.

Upvotes: 0

Related Questions