Skip to content

How To UseEffect

Posted on:February 3, 2023

useEffect is a complicated React hook that I am still learning about. I recently learned how to properly do things on mount while keeping the react-hooks/exhaustive-dep lint rule happy.

Confusing way

In the below code we setup a useEffect that gets triggered whenever nextId changes. This effect will run on mount as well as when we click the button. I think this is kinda confusing because you might not realize clicking the next button will refetch the data (especially as the component grows in size).

function App() {
  const [nextId, setNextId] = useState(1);
  const [data, setData] = useState();

  async function getData(id) {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/' + id);
    const data = await response.json();

    setData(data);
  }

  useEffect(() => {
    getData(nextId);
  }, [nextId]);

  return (
    <div className="App">
        <button onClick={async () => {
            const nextNextId = nextId + 1;
            setNextId(nextNextId)
        }}>
            Next
        </button>
        <p>
            {data?.title}
        </p>
    </div>
  )
}

Better way

The following is a slightly better way of structuring it (in my opinion). You are more explicit as to what happens when the next button is clicked.

function App() {
  const [nextId, setNextId] = useState(1);
  const [data, setData] = useState();

  async function getData(id) {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/' + id);
    const data = await response.json();

    setData(data);
  }

  useEffect(() => {
    getData(nextId);
  }, []); // React Hook useEffect has a missing dependency: 'nextId'.

  return (
    <div className="App">
        <button onClick={async () => {
            const nextNextId = nextId + 1;
            getData(nextNextId);
            setNextId(nextNextId)
        }}>
            Next
        </button>
        <p>
            {data?.title}
        </p>
    </div>
  )
}

Now if we look at the useEffect closer we run into a problem. I only want this to run on mount so it makes sense to specify an empty dependency array. To our dismay, the hooks lint rule is yelling at us to supply a dependency.

useEffect(() => {
    getData(nextId);
}, []); // React Hook useEffect has a missing dependency: 'nextId'.

One non-recommended way to solve this is to eslint disable.

useEffect(() => {
    getData(nextId);
    // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

As an alternative we can add the dependency to make the linter happy.

useEffect(() => {
    getData(nextId);
}, [nextId]);

We have just introduced a bug, can you guess what it is?

Now when we click our button the onClick handler will run as well as the effect which will cause our api call to be done twice. Thats not what we want!

A common way to solve this is use a ref to track the first mount.

useEffect(() => {
   if (!hasMountedRef.current) {
     hasMountedRef.current = true;
     getData(nextId);
   }
 }, [nextId]);

This just seems overly complex and we have another solution up our sleeve.

Best way

The following is what I believe is the best way. Our lint rule is happy and we don’t make duplicate api calls!

const NEXT_ID_INITIAL = 1;

function App() {
  const [nextId, setNextId] = useState(NEXT_ID_INITIAL);
  const [data, setData] = useState();

  async function getData(id) {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/' + id);
    const data = await response.json();

    setData(data);
  }

  useEffect(() => {
    getData(NEXT_ID_INITIAL);
  }, []);

  return (
    <div className="App">
        <button onClick={async () => {
            const nextNextId = nextId + 1;
            getData(nextNextId);
            setNextId(nextNextId)
        }}>
            Next
        </button>
        <p>
            {data?.title}
        </p>
    </div>
  )
}

So let’s break this down a bit. We create our useEffect with an empty dependency array which essentially means run once on mount.

useEffect(() => {
    getData(NEXT_ID_INITIAL);
}, []);

We’ve also moved the initial value outside our component to a variable and provided it to the initial useState

const NEXT_ID_INITIAL = 1;

function App() {
  const [nextId, setNextId] = useState(NEXT_ID_INITIAL);

I think this actually makes a lot of sense. When we call getData on initial mount we are essentially using the initial state. By using that instead of the nextId from useState we are more clear this is a static state and not using a variable that could change.

Other interesting resources

You Might Not Need an Effect