JorgeeFG
JorgeeFG

Reputation: 5941

React useEffect forces me to add dependencies that trigger an infinite loop

Why is React forcing me with their linter plugin to add dependencies that I don't want?

For example, I want my effect to trigger only when a certan value changes, yet the linter tells me to add even functions to the dependencies, and I don't want that.

Why it forces me to do that? What do I gain from that?

  /**
   * Gets all items, pages, until 250th.
   */
  useEffect(() => {
    let mounted = true;
    if (loadUntil250th && !paginationProps.complete) {
      mounted && setLoading(true);
      let limit = 250 - paginationProps.page * BATCH_LIMIT;
      fetchListItems(paginationProps, limit, paginationProps.page * BATCH_LIMIT)
        .then((results) => {
          if (mounted) {
            setPaginationProps({
              ...paginationProps,
              page: 250 / BATCH_LIMIT,
              autoLoad: false,
              complete: paginationProps.totalItems <= 250,
            });
            setListItems(results.listItems);
            setLoading(false);
          }
        })
        .catch((err) => {
          logger.log('LOADMORE FAILED:', err);
          mounted && setPaginationProps({ ...paginationProps, complete: true });
          mounted && setLoading(false);
        });
    }
    return () => {
      mounted = false;
    };
  }, [loadUntil250th]);

It wants this array of dependencies, which result in a infinite loop

[loadUntil250th, logger, paginationProps, setListItems]);

I want to understand why it is required, if I don't want them.

Upvotes: 3

Views: 773

Answers (1)

dev9
dev9

Reputation: 56

The 'exhaustive-deps' lint rule is designed to protect against stale closures, where useEffect references props or state used in the callback but not present in the dependency array. Since logger, paginationProps, and setListItems can theoretically change between renders, it's not safe to reference them inside useEffect without also including them in the dependency array to ensure you're always receiving and acting on up-to-date data. You can think of useEffect as essentially generating a snapshot of all state and props when it gets created and only updating that if one of its dependencies changes.

For instance, without including paginationProps in the dependencies list, if fetchListItems ever modifies the value of paginationProps then useEffect won't have access to that updated value until loadUntil250th changes.

As referenced in this answer, part of the issue is that your usage of useEffect() is unidiomatic. If all you're doing is subscribing to changes to loadUntil250th, you'd be better off moving this function elsewhere and calling it with your code that modifies loadUntil250th.

If you want to keep your code in the useEffect hook, you have a few options:

  1. Assuming that paginationProps and setPaginationProps are derived from a useState hook, you can eliminate the dependency on paginationProps by passing a function to setPaginationProps instead of an object. So your code would become:
setPaginationProps(paginationProps => {
    ...paginationProps,
    page: 250 / BATCH_LIMIT,
    autoLoad: false,
    complete: paginationProps.totalItems <= 250,
});
  1. Move setListItems inside the useEffect hook if possible. This ensures that you can control whatever props/state that function depends on. If that's not possible, you have a few options. You can move the function outside the component altogether to guarantee that it doesn't depend on props or state. Alternatively, you can use the useCallback hook along with the function to control its dependencies and then list setListItems as another dependency.
  2. It's unlikely that the logger function changes between renders, so you're probably safe keeping that inside your dependency array (although it's odd that the linter would expect that).

If you're still curious, this article is helpful at detailing how useEffect and the dependency array actually works.

Upvotes: 2

Related Questions