Reputation: 17711
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
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
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
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