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 */
Related
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
I have a signup/login workflow in React (NextJS), and everything is working correctly; i made a custom hook to remotely check if the user is authenticated based on localStorage jwt:
import React, { useState, useEffect } from 'react';
import axios from '../lib/api';
const useUser = () => {
const [logged, setIsLogged] = useState(false);
const [user, setUser] = useState('');
useEffect(async () => {
const jwt = localStorage.getItem('jwt');
if (!jwt) return;
await axios
.get('/api/users/me', {
headers: {
Authorization: `Bearer ${jwt}`,
},
})
.then((response) => {
setUser(response.data);
setIsLogged(true);
})
.catch((error) => {});
}, []);
return [logged, user, setIsLogged];
};
export default useUser;
This hooks works corectly in 99% of cases, but when i go on login form page, the login form flashes for a sec to logged in users, since the logged status is false before the check is initialized
import React, { useEffect, useState } from 'react';
import useUser from '../../lib/useUser';
import { useRouter } from 'next/router';
import LoginForm from '../../components/LoginForm';
function Login() {
const { push } = useRouter();
const [logged] = useUser();
console.log(ljwt, logged);
if (logged) {
//push('/');
return <p>nored</p>;
}
if (!logged) {
return <LoginForm />;
}
}
export default Login;
how can i avoid this? i tried to pass to useUser the jwt, so it assume the user is logged in while performing the remote check, but it is not really working as expected.
any suggestion?
Don't render the login form when the login state is still indeterminate.
By the way, useEffect functions can't be async in themselves, since they need to either return nothing or a cleanup function; async functions always return a promise.
async function getLoginState() {
const jwt = localStorage.getItem("jwt");
if (!jwt) return [false, null];
const resp = await axios.get("/api/users/me", {
headers: {
Authorization: `Bearer ${jwt}`,
},
});
return [true, response.data];
}
/**
* Get user login state.
*
* Returns undefined if the login state is not yet known.
* Returns a 2-item array [loginState, user] otherwise.
* `user` can be null when `loginState` is false.
*/
function useLoginState() {
const [loginState, setLoginState] = useState(undefined);
useEffect(() => {
getLoginState().then(setLoginState);
}, []);
return loginState;
}
function Login() {
const { push } = useRouter();
const loginState = useLoginState();
if (loginState === undefined) {
return <>Loading...</>;
}
const [logged, user] = loginState;
if (logged) {
return <p>Hi, {JSON.stringify(user)}</p>;
} else {
return <LoginForm />;
}
}
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();
I'm attempting to run a function within the useEffect hook, so that on screen load it automatically calls the context and works out what to do.
But for whatever reason, the function just isn't firing. The screen loads successfully and renders, no errors, but just doesn't do anything.
Here's my component I'm calling the context from:
import React, { useContext, useEffect } from 'react';
import { View, StyleSheet, ActivityIndicator } from 'react-native';
import { AuthContext } from '../context/AuthContext';
const LoadingScreen = ({ navigation }) => {
const { userSignedIn } = useContext(AuthContext)
useEffect(() => {
userSignedIn()
}, [])
return (
<View style={styles.mainView}>
<ActivityIndicator style={styles.indicator} />
</View>
)
}
And my context file:
import React, { useState, useContext } from 'react';
import { navigate } from '../navigationRef';
import { Magic } from '#magic-sdk/react-native';
const m = new Magic('API key');
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState([]);
const userSignedIn = () => {
return async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
}
};
return (
<AuthContext.Provider value={{ user, userSignedIn }}>
{ children }
</AuthContext.Provider>
)
Can anyone point out what I'm doing wrong? Feels a simple one.. But can't figure it out.
You are returning an async function when calling userSignedIn so the following should work for you by making userSignedIn itself async to work for those await calls inside.
const userSignedIn = async () => {
// Call Magic logged in
const loggedIn = await m.user.isLoggedIn();
console.log(loggedIn)
// If user logged in, save details to user, and redirect to dashboard
if (loggedIn === true) {
const { issuer, email } = await m.user.getMetaData();
console.log(issuer)
console.log(email)
setUser([issuer, email])
navigate('authorisedFlow')
// If user not logged in, redirect to login flow
} else {
console.log(userSignedIn)
console.log("Not signed in.")
navigate('loginFlow')
}
};
I am practicing AWS' Cognito. For front-end I am using React and for routing I am using React-router-dom. For Cognito validation I am using amazon-cognito-identity-js package. My Congito signin, signup and confirmation logic works fine. I made one helper function where I validate the Congnito. and reuse it in different component. I split my Nav bar into two components. From Congnito current user I made one callback function and use it in useEffect, and dependencies put the callback function, by default getAuthenticatedUser is null. I add condition where it fetch the data, if getAuthenticatedUser then redirect to signin and signup page. Because of this condition I am getting the error: Can't perform a React state update on an unmounted component...... Also when I signed in it does not change the nav bar name, I have to refresh the browser then I can see the change. I share my code in codesandbox.
This is my helper function
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { CognitoUserPool } from 'amazon-cognito-identity-js';
const Pool_Data = {
UserPoolId: 'us-east-1_IEyFfUupx',
ClientId: '63fc9g5c3g9vhqdalrv9eqhoa2',
};
export default function useHandler() {
const [state, setstate] = useState({
loading: false,
isAuthenticated: false
})
const { loading, isAuthenticated } = state;
const userPool = new CognitoUserPool(Pool_Data)
const getAuthenticatedUser = useCallback(() => {
return userPool.getCurrentUser();
},
[],
);
console.log(getAuthenticatedUser());
useEffect(() => {
getAuthenticatedUser()
}, [getAuthenticatedUser])
const signOut = () => {
return userPool.getCurrentUser()?.signOut()
}
console.log(getAuthenticatedUser());
return {
loading,
isAuthenticated,
userPool,
getAuthenticatedUser,
signOut
}
};
This is my navigation
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import SigninLinks from './SigninLinks';
import SignoutLinks from './SignoutLinks';
import useHandlder from '../configHandler/useHandler';
const Nav = () => {
const { getAuthenticatedUser } = useHandlder();
const Links = getAuthenticatedUser() ? <SigninLinks /> : <SignoutLinks />
return (
<nav className="nav-wrapper grey darken-3">
<div className="container">
<h2 className="brand-logo">Logo</h2>
{
Links
}
</div>
</nav>
);
};
export default Nav;
This is Home screen where it display the data and getting error
import React, { useState, useEffect } from "react";
import { api } from './api';
import useHandlder from './configHandler/useHandler'
import { Redirect } from 'react-router-dom';
const Home = () => {
const [state, setstate] = useState([]);
const { getAuthenticatedUser } = useHandlder();
useEffect(() => {
fetchData()
}, [])
const fetchData = async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts`);
const data = await response.json();
setstate(data)
}
return getAuthenticatedUser() === null ? <Redirect to="/signin" /> : //In here is the //error happening.
<div className="row">
<h1>hello welcome to home</h1>
{
state?.map((i: string, id: number) => <h1 key={id}>{i.title}</h1>)
}
</div>
};
export default Home;
Issue
The issue is your app starts on the home ("/") path and renders the Home component. Home initiates a GET request upon mounting and checks for an authenticated user, and if there is none, renders a redirect to your "/signin" route.
The fetch is asynchronous so when the redirect occurs the GET request is resolving after Home has been unmounted and it tries to update the local state with the response data, but can't.
Solution
You need to use an Abort Controller to cancel in-flight requests. If the component unmounts, an effect cleanup function cancels the fetch request. In Home update the useEffect hook to create an AbortController and signal to be used in a cleanup function.
useEffect(() => {
const controller = new AbortController(); // <-- create controller
const { signal } = controller; // <-- get signal for request
const fetchData = async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts`,
{ signal } // <-- pass signal with options
);
const data = await response.json();
setstate(data);
};
fetchData();
return () => controller.abort(); // <-- return cleanup function to abort
}, []);
Demo