React Hook Function in UseEffect with infinite loop - reactjs

I am trying to call a hook in my App.js file using a hook. All the logic works, but I'm getting a warning error in console "React Hook useEffect has a missing dependency: 'initAuth'." I know there are a lot of issues on this ,but I'm not sure if this is related to the hook or the complexity I am doing at the high level of my app. The intent is to use the "initAuth" function to look at my local storage and get my user token, name, etc... I only want this on a hard page refresh, so it should only run once.
If I add initAuth (the function) or the authObject ( object), I get infinite loops.
function App() {
const { initAuth, authObject } = useAuth();
useEffect(() => {
initAuth();
}, []);
// this throws the warning. I need to add dependency
}

If you only want this effect to run once when the component first loads, then you can ignore the warning. You can disable the warning so it doesn't keep showing up in the console with the following:
useEffect(() => {
initAuth();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

This is how I would implement this hook :
function App() {
const { initialized, authObject, initAuth } = useAuth();
useEffect(() => {
if (!initialized) {
initAuth();
}
}, [initialized, initAuth]);
...
}
Or, better yet :
function App() {
const authObject = useAuth(); // let useAuth initialize itself
...
}
Typically, useAuth seems to be a multi-purpose hook, being used by various components, so it makes no sense to allow multiple components to call initAuth; the hook should only return the current state.
Preferably, you should implement that hook with a context
function App() {
return (
<AuthProvider>
<AppContent />
</AuthProvider>
);
}
function AppContent() {
const authObject = useAuth();
...
}
The contract, therefore, goes to the AuthProvider, and notifies every component using useAuth on state changes.
From OP's own answer, added some suggested improvements :
import React, { createContext, useContext, useState, useMemo } from "react";
const AuthContext = createContext({
isLoggedIn:false /* :Boolean */,
authObject:null /* :Object */,
login: (
username /* :String */,
password /* :String */
) /* :Preomise<Boolean> */ => {
throw new Error('Provider missing');
}
]);
const AuthContextProvider = ({ children }) => {
// init state with function so we do not trigger a
// refresh from useEffect. Use useEffect if the
// initial state is asynchronous
const [state, setState] = useState(() => {
const authObject = localStorage.getItem("authObject");
const isLoggedIn = !!authObject;
return { isLoggedIn, authObject };
});
// avoid refresh if state does not change
const contextValue = useMemo(() => ({
...state, // isLoggedIn, authObject
login: async (username, password) => {
// implement auth protocol, here
// do not expose setState directly in order to
// control what state is actually returned
// setState({ isLoggedIn:..., authObject:... });
// return true|false
}
}), [state]);
return (
<AuthContext.Provider value={ contextValue }>
{ children }
</AuthContext.Provider>
);
};
/**
Usage: const { isLoggedIn, authObject, login } = useAuthContext();
*/
const useAuthContext = () => useContext(AuthContext);
export { useAuthContext, AuthContextProvider };

Thanks to Yanick's comment, this is how I initiated to provider to set my authorization. My login function uses an auth service for http call, but I use this context function to set the data properly.
import React, { useContext, useMemo, useState } from "react";
import http from "services/http";
const AuthContext = React.createContext({});
const AuthContextProvider = ({ children }) => {
const [state, setState] = useState(() => {
const authObject = JSON.parse(localStorage.getItem("authObject"));
if (authObject) {
//sets axios default auth header
http.setJwt(authObject.token);
}
const isLoggedIn = !!authObject;
return { isLoggedIn, authObject };
});
// avoid refresh if state does not change
const contextValue = useMemo(
() => ({
...state, // isLoggedIn, authObject
login(auth) {
localStorage.setItem("authObject", JSON.stringify(auth));
http.setJwt(auth.token);
setState({ authObject: auth, isLoggedIn: true });
return true;
},
logout() {
http.setJwt("");
localStorage.removeItem("authObject");
setState({ authObject: null, isLoggedIn: false });
},
}),
[state]
);
return (
<AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>
);
};
const useAuthContext = () => useContext(AuthContext);
export { useAuthContext, AuthContextProvider };
And my App.js simply uses the ContextProvider, no need to run useEffect anymore on App.js.
<AuthContextProvider>
<ThemeProvider theme={darkState ? dark() : light()}>
<CssBaseline>
<BrowserRouter>
//...app.js stuff
</BrowserRouter>
</CssBaseline>
</ThemeProvider>
</AuthContextProvider>
In any component, I can now get access to isLoggedIn or authObject using a call like:
const { isLoggedIn } = useAuthContext();

Related

The context api resets and re-evaluates from start when I go to another profile route/ next page

AuthContext.js
import { createContext, useEffect, useState } from "react";
import { axiosInstance } from "../../axiosConfig";
import { useCustomToast } from "../../customHooks/useToast";
const initialState = {
user: null,
isLoggedIn: false,
login: () => null,
logOut: () => null,
};
export const AuthContext = createContext(initialState);
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const { showToast } = useCustomToast();
console.log("i am rinning agaon here");
const checkLogin = async () => {
try {
const res = await axiosInstance.get("/auth/refresh");
setIsLoggedIn(true);
console.log("the user is", res?.data);
setUser(res?.data?.user);
} catch (e) {
console.log(e);
setIsLoggedIn(false);
}
};
const logOutHandler = async () => {
try {
const res = await axiosInstance.get("/auth/logout");
showToast(res?.data?.message);
} catch (e) {
showToast("Something went wrong.Please try again");
}
};
useEffect(() => {
checkLogin();
}, []);
const login = (userData) => {
setUser(userData);
setIsLoggedIn(true);
};
const logOut = () => {
setUser(null);
logOutHandler();
setIsLoggedIn(false);
};
return (
<AuthContext.Provider
value={{
user,
isLoggedIn,
login,
logOut,
}}
>
{children}
</AuthContext.Provider>
);
};
ProtectedRoute.js
import React, { useEffect, useContext } from "react";
import { useRouter } from "next/router";
import { AuthContext } from "../context/authContext";
const ProtectedRoute = ({ children }) => {
const { isLoggedIn } = useContext(AuthContext);
const router = useRouter();
useEffect(() => {
if (!isLoggedIn) {
router.push("/login");
}
}, [isLoggedIn]);
return <>{isLoggedIn && children}</>;
};
export default ProtectedRoute;
I am using NextJS and context api for managing user state. Here at first I will check for tokens and if it is valid I will set loggedIn state to true. But suppose I want to go to profile page which is wrapped by protected route, what is happening is AuthContext is resetting and evaluating itself from beginning, the isLoggedIn state is false when I go to /profile route. If I console log isLoggedIn state inside protectedRoute.js, it is false at start and before it becomes true, that router.push("/login) already runs before isLoggedIn becomes true. It feels like all AuthContext is executing again and again on each route change. Is there any code problem? How can I fix it? The one solution I have found is wrapping that wrapping that if(!loggedIn) statement with setTimeOut() of 1 secs so that until that time loggedIn becomes true from context API

Wait for context value to load before making an API call

I have a user context that loads the user data. I am using that data to send API requests in useEffect. The time lag in the loading of the data is causing an undefined variable in my API request. How do I make the useEffect wait for the context variable to load before sending the request?
This is the UserContext.js:
import { createContext, useState } from "react";
const UserContext = createContext({});
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
This is the custom hook:
import { useContext } from "react";
import UserContext from "../context/UserProvider";
const useUser = () => {
return useContext(UserContext);
};
export default useUser;
And this is the API call in the profile page:
const { user } = useUser();
useEffect(() => {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}, []);
How can I ensure user data is loaded before I make a request throughout my app?
In react, Context APi static data will be Passed to the Children at initial Load. But if you are using asynchronous data in context api, you have to use useEffect and add context value as dependency..
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
// Updating Context value Asynchronously..
setTimeout(() => {
setUser("data");
}, [3000]);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
const { user } = useUser();
useEffect(() => {
// Call api only if user contains data.
if(user != {}) {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}
}, [user]);

React useState not updating (immediately)

New to react and hooks, I am trying to do a login module using hooks. However when I am not able to update the state of my Auth state. Read elsewhere that useState do not update immediately and needs to be coupled with useEffect() to have it updated. However I am using useState in a custom hook and not sure how to have updated either through useEffect or other means.
Am I doing some kind of anti-pattern here? Anyone able to help?
const useAuth = () => {
const [auth, setAuth] = useState( {} );
useEffect(()=>{
console.log(auth)
},[])
return {auth, setAuth}
}
export const useHandleLogin = (props) => {
const {auth, setAuth} = useAuth()
const history = useHistory();
useEffect(()=>{
console.log(auth)
},[])
const login = () => {
console.log('login action called---- ');
/* check if user is login */
if(localStorage.getItem('user')){
console.log('got user' );
} else {
console.log('no user, calling backend to authenticate... ' );
// change to login api
axios.get(`http://localhost:3001/projects`)
.then(res => {
console.log('call api' /* + JSON.stringify(res) */);
})
localStorage.setItem('user', JSON.stringify({username:'abc',role:'123'}))
console.log('login done' + JSON.stringify(auth));
console.log('login done2' + auth.authenticated);
}
setAuth({
authenticated: true,
displayName: 'My Name',
email: 'xxx#abc.com',
role: 'admin'
})
console.log("sending to success page" + auth + JSON.stringify(auth)) // Error is here. output is : sending to success page[object Object]{}
import React, {useEffect} from "react";
import { useHandleLogin } from "./LoginUtil"
const TestLoginPage = () => {
const { auth, login } = useHandleLogin();
const Clogin = () => {
console.log('auth: ' + auth)
login();
};
return (
<React.Fragment>
<div>login</div>
<button onClick={Clogin} > Login </button>
</React.Fragment>
);
}
export default TestLoginPage;
Seems you are missing to add some dependency in useEffect() dependency array. Also, in order to generate new instance of a callback like login() you should wrap it in useCallback() and add necessary dependencies in the array. From the look of it,
useEffect() in missing auth in dependency array
login() should be wrapped in useCallback() and must have auth in dependency
array
Clogin() must be wrapped in useCallback() and must have auth and login
in dependency array
You can use add eslint-plugin-react-hooks which can help you in prompting warnings if you miss dependency.
After couple of days of trying and reading up, below is what I believe to be the correct implementation.
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { Redirect } from "react-router-dom";
import axios from 'axios';
export const useAuth = (props) => {
/* const init = localStorage.getItem('user1')? true : false */
const init = {authen : false}
const [auth, setAuth] = useState(init);
const history = useHistory();
console.log("auth initial " + auth)
const checkAuthStatus = () => {
return !!auth.authen
}
const login = () => {
console.log('login action called---- ');
let success = true
if(success){
setAuth({authen : true})
}else{
setAuth ({authen : false})
}
console.log('push history==========' )
/* history.push('/testLoginOK');
history.go(); */
}
const logout = () => {
console.log('logout action called---- ');
setAuth ({authen : false})
history.push('/testLogin');
history.go();
}
useEffect(() => {
console.log("useEffect auth "+auth.authen)
if(!!auth.authen){
/* history.push('/testLoginOK');
history.go(); */
}
})
return {auth, checkAuthStatus, login, logout}
}
/* export default useAuth */

Best way to check if there is already a token in local storage using use effect in React Context

good day. Is this the best way to check if there is already a token in my local storage in my AuthContext? I used a useEffect hook in checking if the token already exists. Should I change the initial state of isAuthenticated since it is always false upon rendering? Im not sure. Thank you
import React, { useState, useContext, useEffect } from "react";
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setAuthenticated] = useState(false);
const login = () => {
setAuthenticated(true);
};
useEffect(() => {
const token = localStorage.getItem("AuthToken");
if (token) {
setAuthenticated(true);
} else if (token === null) {
setAuthenticated(false);
}
return () => {};
}, []);
return <AuthContext.Provider value={{ isAuthenticated, login }}>{children}</AuthContext.Provider>;
};
I would suggest using a state initializer function so you have correct initial state. You won't need to wait until a subsequent render cycle to have the correct authentication state.
const [isAuthenticated, setAuthenticated] = useState(() => {
const token = localStorage.getItem("AuthToken");
return token !== null;
});
The rest can likely remain the same.
I suggest you also provide a default context value as well.
const AuthContext = React.createContext({
isAuthenticated: false,
login: () => {},
});
export function useAuth() {
return useContext(AuthContext);
}
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setAuthenticated] = useState(() => {
const token = localStorage.getItem("AuthToken");
return token !== null;
});
const login = () => setAuthenticated(true);
return (
<AuthContext.Provider value={{ isAuthenticated, login }}>
{children}
</AuthContext.Provider>
);
};

How to combine custom hook for data fetching and context?

I have a custom hook to fetch data on form submit
export const getIssues = ({ user, repo }) => {
const [issues, setIssues] = useState([]);
const handleInputChange = (e) => {
e.preventDefault();
axios.get(`https://api.github.com/repos/${user}/${repo}/issues`)
.then((response) => {
setIssues(response.data);
})
.catch((err) => console.log(err));
};
return {
issues,
onSubmit: handleInputChange,
};
};
In my component I call it like this
const response = getIssues({ user: user.value, repo: repo.value })
return (
<form className={css['search-form']} {...response}>...</form>
)
The problem is that I want to get my issues value from the hook in another component. For that I wanted to use Context. But I have no idea how to do it.
I could call this function and pass it to Provider, but I can't call it without arguments. So I kind of stuck.
All the help will be much appreciated.
You are right by saying you need React.Context to handle this situation.
You need to wrap your components into this context.
import React from "react";
const IssuesStateContext = React.createContext();
const IssuesDispatchContext = React.createContext();
function issuesReducer(state, action) {
switch (action.type) {
case "setIssues": {
return [...action.payload];
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
function IssuesProvider({ children }) {
const [state, dispatch] = React.useReducer(issuesReducer, []);
return (
<IssuesStateContext.Provider value={state}>
<IssuesDispatchContext.Provider value={dispatch}>
{children}
</IssuesDispatchContext.Provider>
</IssuesStateContext.Provider>
);
}
function useIssuesState() {
const context = React.useContext(IssuesStateContext);
if (context === undefined) {
throw new Error("useIssuesState must be used within a IssuesProvider");
}
return context;
}
function useIssuesDispatch() {
const context = React.useContext(IssuesDispatchContext);
if (context === undefined) {
throw new Error("useIssuesDispatch must be used within a IssuesProvider");
}
return context;
}
export { IssuesProvider, useIssuesState, useIssuesDispatch };
By using this separation in context you will be able to set issues coming from github in one component and render them in a completely different one.
Example:
App.js
ReactDOM.render(
<IssuesProvider>
<Component1 />
<Component2 />
</IssuesProvider>
)
Component 1
import React from 'react'
import { useIssuesDispatch } from './issues-context'
function Component1() {
const dispatch = useIssuesDispatch()
// fetch issues
// .then dispatch({ type: 'setIssues', payload: response })
// render
}
Component 2
import React from 'react'
import { useIssuesState } from './issues-context'
function Component2() {
const issues = useIssuesState()
// if issues.length > 0 ? render : null
}
You can write a Issues context provider that will provide {issues,useIssues} where issues are the issues and useIssues is a function that takes {user,repo}.
export const Issues = React.createContext();
export default ({ children }) => {
const [issues, setIssues] = useState([]);
const useIssues = ({ user, repo }) => {
useEffect(() => {
axios
.get(
`https://api.github.com/repos/${user}/${repo}/issues`
)
.then(response => {
setIssues(response.data);
})
.catch(err => console.log(err));
}, [user, repo]);
return issues;
};
return (
<Issues.Provider value={{ issues, useIssues }}>
{children}
</Issues.Provider>
);
};
The component that has all the components that need issues can import this issues provider:
import IssuesProvider from './IssuesProvider';
export default () => (
<IssuesProvider>
<ComponentThatNeedsIssues />
<ComponentThatSetsAndGetsIssues />
</IssuesProvider>
);
For a component that needs to set issues you can get useIssues from context:
const { useIssues } = useContext(Issues);
const issues = useIssues({user,repo});
For a component that only needs issues:
const { issues } = useContext(Issues);
To see it all work together there is a codepen here

Resources