MarcoS
MarcoS

Reputation: 17711

How to avoid "React Hook useEffect has a missing dependency" and infinite loops

I have written a react-js component like this:

import Auth from "third-party-auth-handler";
import { AuthContext } from "../providers/AuthProvider";

export default function MyComponent() {
  const { setAuth } = useContext(AuthContext);

  useEffect(() => {
    Auth.isCurrentUserAuthenticated()
      .then(user => {
        setAuth({isAuthenticated: true, user});
    })
    .catch(err => console.error(err));
  }, []);
};

With the following AuthProvider component:

import React, { useState, createContext } from "react";

const initialState = { isAuthenticated: false, user: null };
const AuthContext = createContext(initialState);

const AuthProvider = (props) => {
  const [auth, setAuth] = useState(initialState);

  return (
    <AuthContext.Provider value={{ auth, setAuth }}>
      {props.children}
    </AuthContext.Provider>
  )
};

export { AuthProvider, AuthContext };

Everything works just fine, but I get this warning in the developer's console:

React Hook useEffect has a missing dependency: 'setAuth'. Either include it or remove the dependency array react-hooks/exhaustive-deps

If I add setAuth as a dependency of useEffect, the warning vanishes, but I get useEffect() to be run in an infinite loop, and the app breaks out.
I understand this is probably due to the fact that setAuth is reinstantiated every time the component is mounted.
I also suppose I should probably use useCallback() to avoid the function to be reinstantiated every time, but I really cannot understand how to use useCallback with a function from useContext()

Upvotes: 3

Views: 1323

Answers (3)

MarcoS
MarcoS

Reputation: 17711

I did finally solve not using useCallback in MyComponent, but in ContextProvider:

import React, { useState, useCallback, createContext } from "react";

const initialState = { authorized: false, user: null };

const AuthContext = createContext(initialState);

const AuthProvider = (props) => {
  const [auth, setAuth] = useState(initialState);
  const setAuthPersistent = useCallback(setAuth, [setAuth]);

  return (
    <AuthContext.Provider value={{ auth, setAuth: setAuthPersistent }}>
      {props.children}
    </AuthContext.Provider>
  )
};

export { AuthProvider, AuthContext };

I am not sure this is the best pattern, because code is not so straightforward and self-explaining, but it works, with no infinite loop nor any warning...

Upvotes: 0

MistyK
MistyK

Reputation: 6222

If you want to run useEffect call just once when component is mounted, I think you should keep it as it is, there is nothing wrong in doing it this way. However, if you want to get rid of the warning you should just wrap setAuth in useCallback like you mentioned.

const setAuthCallback = useCallback(setAuth, []);

And then put in in your list of dependencies in useEffect:

useEffect(() => {
    Auth.isCurrentUserAuthenticated()
      .then(user => {
        setAuth({isAuthenticated: true, user});
    })
    .catch(err => console.error(err));
  }, [setAuthCallback]);

If you have control over AuthContext Provider, it's better to wrap your setAuth function inside.

After OP edit: This is interesting, setAuth is a function from useState which should always be identical, it shouldn't cause infinite loop unless I'm missing something obvious

Edit 2:

Ok I think I know the issue. It seems like calling

setAuth({ isAuthenticated: true, user });

is reinstantianting AuthProvider component which recreates setAuth callback which causes infinite loop. Repro: https://codesandbox.io/s/heuristic-leftpad-i6tw7?file=/src/App.js:973-1014

In normal circumstances your example should work just fine

Upvotes: 2

underscore
underscore

Reputation: 6887

This is the default behavior of useContext. If you are changing the context value via setAuth then the nearest provider being updated with latest context then your component again updated due to this.

To avoid this re-rendering behavior you need to memorize your component.

This is what official doc says

Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest <MyContext.Provider> above the calling component in the tree.

When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.

Like this ?

function Button() {
  let appContextValue = useContext(AppContext);
  let theme = appContextValue.theme; // Your "selector"

  return useMemo(() => {
    // The rest of your rendering logic
    return <ExpensiveTree className={theme} />;
  }, [theme])
}

Upvotes: 1

Related Questions