Persistent Authentication React using UseEffect and Intervals - function not firing - reactjs

I am attempting to leverage my refresh token backend point within my JWT setup for my React app.
The AuthContext file below works well beside the useEffect and interval. It would seem that my attempt to check the validity of the token every 10 secs is not working. Neither console logs in the refresh function are logged. All ideas very welcome.
import React, { useEffect, useState, useRef} from 'react';
import RefreshToken from '../Services/RefreshToken';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
rToken: '',
tokenExpiry:'',
login: (token) => {},
logout: () => {},
});
export const AuthContextProvider = (props) => {
const [token, setToken] = useState(null);
const [refreshToken, setrefreshToken] = useState(null)
const [tokenExpiry, setTokenExpiry] = useState(null)
let interval = useRef(null);
const userIsLoggedIn = !!token;
const loginHandler = (token,rToken) => {
setToken(token);
setrefreshToken(rToken);
var current = new Date();
current.setHours(current.getHours(),current.getMinutes()+4,0,0);
setTokenExpiry(current)
};
const logoutHandler = () => {
setToken(null);
setrefreshToken(null);
};
const contextValue = {
token: token,
isLoggedIn: userIsLoggedIn,
rToken: refreshToken,
tokenExpiry: tokenExpiry,
login: loginHandler,
logout: logoutHandler,
};
const refresh =(timenow)=>{
console.log("checking")
if(tokenExpiry <= timenow){
console.log("refresh")
try {
const response = RefreshToken(token, refreshToken);
loginHandler(response.token, response.refreshToken);
} catch (err) {
console.log(err);
logoutHandler();
}
}
}
useEffect(() => {
const timeNow = new Date();
interval.current = setInterval(refresh(timeNow), 10000);
return () => clearInterval(interval.current);
}, [])
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;

Related

I need to refresh the page to login | React and Axios

I have a problem when I want to log in to the login by entering the email and password. What happens is that when I enter with the correct email and correct password, the animation appears but it stays cycled, and if I refresh the page and try again, now it lets me enter into the application
Here's my login form code:
import axios from "axios";
import { useRef, useState } from "react";
import { storeToken } from "../utils/authServices";
import { useNavigate } from "react-router-dom";
import { useLoading } from "../context/hooks/useLoading";
import { LoginForm } from "../components";
export const Login = () => {
const API_URL = "https://api.app"; //I hide the API for security reasons
const { run } = useLoading();
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const navigate = useNavigate();
const correoRef = useRef("");
const passwordRef = useRef("");
const handleSubmit = async (e) => {
e.preventDefault();
const { value: correo } = correoRef.current;
const { value: password } = passwordRef.current;
await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
})
.then((response) => {
storeToken(response.data.token);
run();
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
})
.catch((err) => {
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
});
};
return (
<LoginForm
correoRef={correoRef}
passwordRef={passwordRef}
handleSubmit={handleSubmit}
error={error}
errorMessage={errorMessage}
/>
);
};
import { createContext, useReducer, useContext } from "react";
const initialState = {
loading: false,
alerts: [],
};
const reducers = (state, action) => {
switch (action.type) {
case "LOADING_RUN":
return {
...state,
loading: true,
};
case "LOADING_STOP":
return {
...state,
loading: false,
};
default:
return { ...state };
}
};
const AppContext = createContext();
const AppContextProvider = (props) => {
const [state, dispatch] = useReducer(reducers, initialState);
return <AppContext.Provider value={{ state, dispatch }} {...props} />;
};
const useAppContext = () => useContext(AppContext);
export { AppContextProvider, useAppContext };
import { useMemo } from "react";
import { useAppContext } from "../AppContext";
export const useLoading = () => {
const { dispatch } = useAppContext();
const loading = useMemo(
() => ({
run: () => dispatch({ type: "LOADING_RUN" }),
stop: () => dispatch({ type: "LOADING_STOP" }),
}),
[dispatch]
);
return loading;
};
import jwt_decode from "jwt-decode";
export const storeToken = (token) => {
localStorage.setItem("token", token);
};
export const getToken = (decode = false) => {
const token = localStorage.getItem("token");
if (decode) {
const decoded = jwt_decode(token);
return decoded;
}
return token;
};
export const logout = () => {
localStorage.removeItem("token");
};
How can I log in without refreshing the page?
There's two problems here. One is you're using await with a .then .catch block. Pick one or the other. You're also never calling the stop() dispatch when your async call is complete which appears to be responsible for removing the loader.
Instead of:
const { run } = useLoading();
Use:
const { run, stop } = useLoading();
Then change this:
setTimeout(() => {
navigate("/nueva-solicitud");
}, 1000);
To this:
setTimeout(() => {
navigate("/nueva-solicitud");
stop();
}, 1000);
Although I would just recommend writing the entire promise like this:
try {
run();
const response = await axios
.post(`${API_URL}/api/auth/login/`, {
correo,
password,
});
storeToken(response.data.token);
navigate("/nueva-solicitud");
stop();
} catch (err) {
stop();
console.log(err.response.data);
setError(true);
setErrorMessage(err.response.data.msg);
}

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

React : Value inside useEffect not defined

So I am building an e-commerce website checkout page with commerce.js. I have a context that allows me to use the cart globally. But on the checkout page when I generate the token inside useEffect , the cart variables have not been set until then.
My context is as below
import { createContext, useEffect, useContext, useReducer } from 'react';
import { commerce } from '../../lib/commerce';
//Provides a context for Cart to be used in every page
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = 'SET_CART';
const initialState = {
id: '',
total_items: 0,
total_unique_items: 0,
subtotal: [],
line_items: [{}],
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
useEffect(() => {
getCart();
}, []);
const getCart = async () => {
try {
const cart = await commerce.cart.retrieve();
setCart(cart);
} catch (error) {
console.log('error');
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={state}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
Now on my checkout page
const CheckoutPage = () => {
const [open, setOpen] = useState(false);
const [selectedDeliveryMethod, setSelectedDeliveryMethod] = useState(
deliveryMethods[0]
);
const [checkoutToken, setCheckoutToken] = useState(null);
const { line_items, id } = useCartState();
useEffect(() => {
const generateToken = async () => {
try {
const token = await commerce.checkout.generateToken(id, {
type: 'cart',
});
setCheckoutToken(token);
} catch (error) {}
};
console.log(checkoutToken);
console.log(id);
generateToken();
}, []);
return <div> {id} </div>; //keeping it simple just to explain the issue
};
In the above code id is being rendered on the page, but the token is not generated since on page load the id is still blank. console.log(id) gives me blank but {id} gives the actual value of id
Because CheckoutPage is a child of CartProvider, it will be mounted before CartProvider and the useEffect will be called in CheckoutPage first, so the getCart method in CartProvider hasn't been yet called when you try to read the id inside the useEffect of CheckoutPage.
I'd suggest to try to call generateToken each time id changes and check if it's initialised first.
useEffect(() => {
if (!id) return;
const generateToken = async () => {
try{
const token = await commerce.checkout.generateToken(id, {type: 'cart'})
setCheckoutToken(token)
} catch(error){
}
}
console.log(checkoutToken)
console.log(id)
generateToken()
}, [id]);

React typescript app how to stop logouts in refresh?

I am working in a react app built with typescript which takes Django api for log in. I am storing the json web token in my local storage. But the app still logs out automatically when I refresh the page.
Here is what I have done
Django urls.py
path('login/', obtain_jwt_token),
This api is called in react logInContainer. I am saving the token in my local storage.
const logInContainer: React.FC = () => {
const dispatch = useDispatch();
const api = new Api();
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [errorText, setError] = React.useState('');
const signIn = async () => {
const res = await api.post('/login/', {
username: username,
password: password,
});
const json = JSON.stringify(res);
localStorage.setItem("user-info", json);
if (res) {
dispatch(logInAction(res.token));
} else {
setError('login failed');
}
}
My logInAction
export const LogInAction = (token: string): AuthTypes => {
return {
type: AuthTypes.logIn,
payload: {
token: token,
}
};
};
My authTypes.ts
export const AuthTypes= {
logIn: "SIGN_IN",
logOut: "SIGN_OUT",
} as const;
So far, the login works fine. and the token is also stored in local storage. But whenever I refresh my page, the app log outs automatically. I need to solve this issue. Any help regarding this will be appreciated.
Here is how logout happens
const logOut = async () => {
dispatch(logOutAction())
};
This is called by
<IconButton onClick={logOut}>
<ExitToApp />
</IconButton>
here is logOutAction
export const logOutAction = (): AuthTypes => {
return {
type: ActionTypes.logOut,
};
};
In my reducer ts
import { AuthState, AuthTypes } from "./types";
const initialState: AuthState = {
token: '',
isSignIn: false,
};
Which goes to authTypes
case ActionTypes.signIn:
return Object.assign({}, state, action.payload, { isSignIn: true });
Could you please try to change your reducer code as given below
import { AuthState, AuthTypes } from "./types";
const initialState: AuthState = {
token : localStorage.getItem('token')? localStorage.getItem('token') : '',
isSignIn : localStorage.getItem('token')? true : false,
};

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>
);
};

Resources