useEffect hook - dependencies - re-render issues

Viewed 5021

The first case:
_Let's say I have a prop that is in redux state or in parent state.
_I do not want the useEffect to fire whenever this prop changes,
_But I do need to use the prop within the useEffect.
_React warns me to add the prop to the dependency array, but if I do so, then the useEffect will fire again.

The second case:
_I am using a function within a useEffect,
_But the function is also needed elsewhere.
_Don't want to duplicate the function code.
_React wants me to add the function to the dependency array, but I don't want the useEffect to fire every time that function reference changes.

4 Answers

While working with useEffect you should think about closures. If any props or local variable is used inside the useEffect then it is advised to include it inside your dependency array else you will be working with the stale data due to the closure.

Here I present a use case. Inside ComponentUsingRef we are using ref which works as the container. You can find more about it at https://reactjs.org/docs/hooks-reference.html#useref


Advantage of this approach is that you wont be bound to memoize fn in your parent component. You will always be using latest value of function and in the given case it won't even cause firing of useEffect as it won't be on your dependency


const Component=({fn})=>{
    useEffect(()=>{
       fn()
    },[fn])
    .....
    .....
    return <SomeComponent {...newProps}/>
}

const ComponentUsingRef=({fn}){
    const fnRef = useRef(fn)
    fnRef.current = fn // so that our ref container contains the latest value
    useEffect(()=>{
       fn.current()
    },[ ])
    .....
    .....
    return <SomeComponent {...newProps}/>
}

If you want to use abstraction then you should extract your logic inside the custom hook

If the only issue is the warning then don't worry about it, you can simply disable the rule for your effect and omit the prop from the dependency array if it's the only prop being used in the effect. For effects with multiple dependencies, you can use useRef to store the latest value of the prop without firing the effect, like this:

const Comp = ({someProp, ...props}) => {
  const myProp = useRef(someProp);
  myProp.current = someProp;

  // this effect won't fire when someProp is changed
  React.useEffect(() => {
    doSomething(myProp.current, props.a, props.b);
  }, [props.a, props.b, myProp]);
}

For your second case, I'd probably put that function in a separate file and import it in the components where it'll be used, however, you can use the same pattern for your second case.

To ignore the warnings react gives you, you can disable them by adding // eslint-disable-next-line react-hooks/exhaustive-deps to the useEffect code. You can read more about this in the Rules of Hooks section in the React documentation. These rules are included in the eslint-plugin-react-hooks package.

To prevent the useEffect from firing everytime the function reference changes, you can use useCallback. The useCallback hook will store the reference to the function instead of the function itself. The reference of the function will only be updated when one of the dependencies of the function is updated. If you don't want the function reference to be updated ever, you can leave the dependency array empty in the same way as the dependency array of the useEffect hook.

Here is a working example of both:

import React, { useState, useEffect, useCallback } from "react";
import "./styles.css";

export default function App() {
  const [value, setValue] = useState(0);

  const wrappedFunction = useCallback(
    (caller) => {
      const newValue = value + 1;
      setValue(newValue);
      console.log(`Firing function from ${caller}: ${newValue}`);
    },
    [value]
  );

  useEffect(() => {
    console.log(`Logging from useEffect: ${value}`);
    wrappedFunction("useEffect");
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <div>{value}</div>
      <button onClick={() => wrappedFunction("button click")}>
        Fire function
      </button>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

Refer this link for the First Problem Sandbox

Here we are just using a state variable to hold the initial value, whether its coming from the Parent or Store, so Even if the value change inside the Parent or store, the child will still be on the older value, until unless it updates it using useEffect.

Parent

export default function App() {
  const [state, setState] = useState("monika");
  return (
    <div className="App">
      <h1>My Name is {state}</h1>
      <button
        onClick={() => {
          setState("Rohan");
        }}
      >
        change name
      </button>
      <First username={state} />
    </div>
  );
}

Child

export default function First({ username }) {
  const [name, setName] = useState(username);
  return (
    <div>
      <h1>My Name is {name}</h1>
    </div>
  );
}
Related