sajad torkamani

Why functions as dependencies require some thought

Sometimes your effects invoke another function and assuming you have the exhaustive-deps ESLint rule enabled, you’ll get warnings about missing dependencies if you don’t include the function in the dependencies array.

But if you include the function as a dependency, you effect will likely run on every render because the function identity will be different on each render (unless you wrap the function in useCallback).

These are some approaches I usually take, though the React docs mention a few others that can be useful.

1. Move functions that don’t need props or state outside your component

If the function doesn’t read props or state values, you can move it outside the component and safely use it inside useEffect.

2. Move your function inside the effect

When your function is outside the useEffect hook, it can be difficult to keep track of what state or props that function depends on.

For example, in the following snippet, you must check the fetchProduct for any props or state it depends on (productId in this case) and remember to pass those in the dependencies array to useEffect.

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  async function fetchProduct() {
    const response = await fetch('http://myapi/product/' + productId); // Uses productId prop
    const json = await response.json();
    setProduct(json);
  }

  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 Invalid because `fetchProduct` uses `productId`
  // ...
}

It’s easy to forget to pass a valid dependency.

You can make your life easier by moving the fetchProduct function inside the useEffect hook and ensuring you have the exhaustive-deps ESLint rule enabled. Now, ESLint can analyze your code for you and warn you if you forgot to add a dependency.

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);

  useEffect(() => {
    // By moving this function inside the effect, we can clearly see the values it uses.
    async function fetchProduct() {
      const response = await fetch('http://myapi/product/' + productId);
      const json = await response.json();
      setProduct(json);
    }

    fetchProduct();
  }, [productId]); // ✅ Valid because our effect only uses productId
  // ...
}

By delegating these checks to ESLint, you’ll have fewer bugs caused by forgotten dependencies.

3. Wrap the function in a useCallback hook

Sometimes, it’s not possible to move a function inside the useEffect. In these cases, you can add the function to the effect’s dependencies, but wrap its definition into a useCallback.

Wrapping the function in useCallback will ensure that the function identity remains the same between renders unless one of the functions’ dependencies changes.

function ProductPage({ productId }) {
  // ✅ Wrap with useCallback to avoid change on every render
  const fetchProduct = useCallback(() => {
    // ... Does something with productId ...
  }, [productId]); // ✅ All useCallback dependencies are specified

  return <ProductDetails fetchProduct={fetchProduct} />;
}

function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); // ✅ All useEffect dependencies are specified
  // ...
}

Sources