learningtech
learningtech

Reputation: 33685

useEffect warns about missing dependency which causes infinite loop

The following code works perfectly :

import { useState, useEffect } from 'react';

const Main = () => {

  const [ form, setForm ] = useState({
    error: {},
    data: {}
  });

  useEffect( () => {
    async function fetchData() {
      const promise = await fetch(`test.json`);
      const result = await promise.json();
      const newForm = {...form};
      newForm.data = result;
      setForm(newForm);
      console.log('executed');
    }
    fetchData();
  }, []);  // *** I will speak to this [] second argument shortly in question below

  return (
    <div>
      <p>{Object.keys(form.data).length}</p>
    </div>
  );
};

All it does is on component mount, grab a test.json file that has the content {"data":"hello"}. This works perfectly and does what I want.

However, in my console, I see the compiler complain with this message Line 20:6: React Hook useEffect has a missing dependency: 'form'. Either include it or remove the dependency array react-hooks/exhaustive-deps. When I add [ form ] as the second argument to useEffect or if I delete the [] second argument from useEffect, then the useEffect goes into infinite loop.

Why is my compiler warning me of an issue and suggesting an action that causes an infinite loop?

Upvotes: 1

Views: 576

Answers (1)

Jacob Smit
Jacob Smit

Reputation: 2379

This error / warning is created by your linter.

The linter rule assumes that you have missed a variable that is external to the useEffect in the dependency array which would cause unexpected outcomes.

You could disable the lint rule for:

  1. The line
useEffect(() => {

}, []); // eslint-disable-line react-hooks/exhaustive-deps
  1. The rest of the file
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {

}, []);
  1. All files using your eslintrc file.

If you don't want to disable the rule you could swap to using the setState callback syntax which provides the current state as a parameter.

import { useState, useEffect } from 'react';

const Main = () => {

  const [ form, setForm ] = useState({
    error: {},
    data: {}
  });

  useEffect( () => {
    async function fetchData() {
      const promise = await fetch(`test.json`);
      const result = await promise.json();
      setForm(currentForm => {
        const newForm = {...currentForm};
        newForm.data = result;
        return newForm;
      });
      console.log('executed');
    }
    fetchData();
  }, []);

  return (
    <div>
      <p>{Object.keys(form.data).length}</p>
    </div>
  );
};

This removes the need to include form in the useEffect.

As to the reason why the linter might think this is an issue, look at this example:

export default function App() {
  const [data, setData] = useState({ a: 'Initial Value', b: null });
  const [control, setControl] = useState({ a: "Initial Value", b: null });

  useEffect(() => {
    const asyncFunc = () => {
        new Promise(resolve => {
          setTimeout(() => resolve(true), 2000) 
        })
        .then(() => {
          // The value of "data" will be the initial value from 
          // when the useEffect first ran.
          setData({...data, b: 'Async Updated'});

          // The value of "current" wille be the current value of
          // the "control" state.
          setControl(current => ({ ...current, b: "Async Updated" }));
        })
    };

    asyncFunc();
    
    // Update the data state while the async function has not
    // yet been completed.
    setData({ a: 'Changed Value', b: null });
    
    // Update the control state while the async function has not
    // yet been completed.
    setControl({ a: "Changed Value", b: null });
  }, []);

  // The data value will swap to "Changed Value" and then back 
  // to "Initial Value" (unexpected) once the async request is
  // complete.
  // As the control used the current value provided by the 
  // callback it is able to avoid this issue. 
  return (
    <div>
      Data:
      <pre>{JSON.stringify(data, null, "\t")}</pre>
      Control:
      <pre>{JSON.stringify(control, null, "\t")}</pre>
    </div>
  );
};

You can run this example here: https://stackblitz.com/edit/react-hmfddo

Upvotes: 4

Related Questions