3

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()

skyboyer
  • 22,209
  • 7
  • 57
  • 64
MarcoS
  • 17,323
  • 24
  • 96
  • 174
  • Could you show the AuthContext code? – raina77ow Jul 04 '21 at 17:45
  • 1
    To show you how to use `useCallback`, we'll need to see where `setAuth` is defined (ie, where you render an `AuthContext.Provider`) – Nicholas Tower Jul 04 '21 at 17:45
  • What official pattern did you follow here (whose docs explain that you should do things this way)? Because effects are for side-effects that should happen when a component instance updates. You shouldn't make other code call effects directly, that code should instead update the owning component, so that _it_ triggers the appropriate side effect. – Mike 'Pomax' Kamermans Jul 04 '21 at 18:10
  • I am not seeing an infinite loop - https://codesandbox.io/s/use-context-infinite-loop-q5xk4. The code is exactly as you have provided. – A G Jul 04 '21 at 18:27
  • @Mike'Pomax'Kamermans: you are right, I did probably mix some different documentation sources and examples... Do you mean I should not call setAuth in the useEffect() in MyComponent? – MarcoS Jul 04 '21 at 19:24

3 Answers3

2

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

MistyK
  • 6,055
  • 2
  • 42
  • 76
  • Yes, I have control over `AuthContext`, I'll add it to the question... Thanks for your answer! – MarcoS Jul 04 '21 at 17:48
  • I had to use `setAuthCallback({isAuthenticated: true, user})` instead of `setAuth({isAuthenticated: true, user})` `in the useEffect`, and `const setAuthCallback = useCallback(setAuth, [setAuth])` (with the setAuth dependency) to avoid an additional warning. Now it works, and no warnings. But I'd prefer to wrap the `setAuth` inside the `AuthContext` in `useCallback()`, if possible,... – MarcoS Jul 04 '21 at 18:01
  • I can assure you it **does** cause an infinite loop... :-) – MarcoS Jul 04 '21 at 18:12
  • I see. But how to let my example work in the circustances I did explain? – MarcoS Jul 04 '21 at 18:55
  • @Marcos I think the crucial part is that - why is this actually happening in your example? Why AuthProvider gets recreated (not rerendered) when setAuth is called? – MistyK Jul 04 '21 at 19:11
1

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])
}
underscore
  • 6,495
  • 6
  • 39
  • 78
0

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...

MarcoS
  • 17,323
  • 24
  • 96
  • 174
  • Did it really solve the issue? It doesn't really make much sense - setAuthPersistent is always equivalent to setAuth. It should cause the same issues which you had before. – MistyK Jul 04 '21 at 20:42
  • @Mistyk: you're right! I did try to use simply setAuth again, in context provider, and everything works smootly, no warning and no loop... I'm afraid I can't understand what was changed from the situation with the loop... Sorry everybody who worked on this thread... :-( – MarcoS Jul 05 '21 at 06:56
  • I told you it's suspicious :) – MistyK Jul 05 '21 at 07:16