How to do fetch with React Hooks; ESLint enforcing `exhaustive-deps` rule, which causes infinite loop

Viewed 1093

I'm pretty new to React hooks in general, and very new to useSelector and useDispatch in react-redux, but I'm having trouble executing a simple get request when my component loads. I want the get to happen only once (when the component initially loads). I thought I knew how to do that, but I'm running into an ESLint issue that's preventing me from doing what I understand to be legal code.

I have this hook where I'm trying to abstract my state code:

export const useState = () => {
  const dispatch = useDispatch();
  const data = useSelector((state) => state.data);

  return {
    data: data,
    get: (props) => dispatch(actionCreators.get(props))
  };
};

Behind the above function, there's a network request that happens via redux-saga and axios, and has been running in production code for some time. So far, so good. Now I want to use it in a functional component, so I wrote this:

import * as React from 'react';
import { useState } from './my-state-file';

export default () => {
  const myState = useState();

  React.useEffect(
    () => {
      myState.get();
      return () => {};
    },
    []
  );
  return <div>hello, world</div>;
};

What I expected to happen was that because my useEffect has an empty array as the second argument, it would only execute once, so the get would happen when the component loaded, and that's it.

However, I have ESLint running on save in Atom, and every time I save, it changes that second [] argument to be [myState], the result of which is:

import * as React from 'react';
import { useState } from './my-state-file';

export default () => {
  const myState = useState();

  React.useEffect(
    () => {
      myState.get();
      return () => {};
    },
    [myState]
  );
  return <div>hello, world</div>;
};

If I load this component, then the get runs every single render, which of course is the exact opposite of what I want to have happen. I opened this file in a text editor that does not have ESLint running on save, so when I was able to save useEffect with a blank [], it worked.

So I'm befuddled. My guess is the pattern I'm using above is not correct, but I have no idea what the "right" pattern is.

Any help is appreciated.

Thanks!

UPDATE:

Based on Robert Cooper's answer, and the linked article from Dan Abramov, I did some more experimenting. I'm not all the way there yet, but I managed to get things working.

The big change was that I needed to add a useCallback around my dispatch functions, like so:

export const useState = () => {
  const dispatch = useDispatch();
  const data = useSelector((state) => state.data);
  const get = React.useCallback((props) => dispatch({type: 'MY_ACTION', payload:props}), [
    dispatch
  ]);

  return {
    data: data,
    get: get,
  };
};

I must admit, I don't fully understand why I need useCallback there, but it works.

Anyway, then my component looks like this:

import * as React from 'react';
import { useState } from './my-state-file';

export default () => {
  const {get, data}  = useState();

  React.useEffect(
    () => {
      get();
      return () => {};
    },
    [get]
  );
  return <div>{do something with data...}</div>;
};

The real code is a bit more complex, and I'm hoping to abstract the useEffect call out of the component altogether and put it into either the useState custom hook, or another hook imported from the same my-state-file file.

1 Answers

I believe the problem you're encountering is that the value of myState in your dependency array isn't the same value or has a different JavaScript object reference on every render. The way to get around this would be to pass a memoized or cached version of myState as a dependency to your useEffect.

You could try using useMemo to return a memoized version of your state return by your custom useState. This might look something like this:

export const useState = () => {
  const dispatch = useDispatch();
  const data = useSelector((state) => state.data);

  return useMemo(() => ({
    data: data,
    get: (props) => dispatch(actionCreators.get(props))
  }), [props]);
};

Here's what Dan Abramov has to say regarding infinite loops in useEffect methods:

Question: Why do I sometimes get an infinite refetching loop?

This can happen if you’re doing data fetching in an effect without the second dependencies argument. Without it, effects run after every render — and setting the state will trigger the effects again. An infinite loop may also happen if you specify a value that always changes in the dependency array. You can tell which one by removing them one by one. However, removing a dependency you use (or blindly specifying []) is usually the wrong fix. Instead, fix the problem at its source. For example, functions can cause this problem, and putting them inside effects, hoisting them out, or wrapping them with useCallback helps. To avoid recreating objects, useMemo can serve a similar purpose.

Full article here: https://overreacted.io/a-complete-guide-to-useeffect/

Related