React useState in context does not update - javascript

I'm using React to create a login page which after logging in should keep track if the user is logged in. I figured using the react context and state hook would be easier than using something as big and complex as Redux.
I created a login function which works, and after a successfull login it should update my state in my context. The login works (i get a status 200) but my state is not updated in my context.
My 'AuthContext.jsx' looks like this
import React, { createContext, useState } from "react";
import { login, register } from "../API/apiMethods";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [authenticated, setAuthenticated] = useState(false);
const authSetter = (state) => {
setAuthenticated(state);
};
const authGetter = () => {
return authenticated;
};
return (
<AuthContext.Provider
value={{
authSetter,
authGetter,
login,
register,
}}
>
{children}
</AuthContext.Provider>
);
};
My login function looks like this
/**
* #description Handles login
* #param {String} email
* #param {String} password
* #returns {Boolean}
*/
export const login = async(email, password) => {
try {
let authenticated = false;
await fetch(BASE_URL + "login", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
password
}),
}).then((res) => {
authenticated = res.status === 200 ? true : false;
});
return authenticated;
} catch (err) {
console.log(err);
return false;
}
};
And in my login form i try to update the authentication boolean after a successfull login
const {
login,
authSetter,
authGetter
} = useContext(AuthContext);
const submit = async(e) => {
e.preventDefault();
await login(email, password)
.then((authSuccess) => {
if (authSuccess) {
console.log("login successfull");
authSetter(true);
}
})
.then(() => console.log(authGetter()));
};
With this code i expected the console output to be a printed string with 'login successfull' and a printed boolean true.
But it seems my state was not updated even though i did call the setter.
I don't know why it won't update, can anyone help me?

This piece of code does exactly what it is supposed to. When you update the state it does not happen immediately.
const submit = async(e) => {
e.preventDefault();
await login(email, password)
.then((authSuccess) => {
if (authSuccess) {
console.log("login successfull");
authSetter(true);
}
})
.then(() => console.log(authGetter()));
};
When you call the authSetter(true);, the state update is queued and once the then callback completes it goes to the next then in the chain which has your authGetter(). Now the state update does not happen immediately as I explained, it is queued. So when the last then callback is executed the state update which is queued has not happened and you still see false which is the old value.
You can refactor your AuthProvider in the following way, there is no need to wrap the setter in a function as it would create a new instance of the function when the state is updated (useState on the other hand returns a memoized value of the setter function) and you can simply return the authenticated state without the getter which again has the same issue.
import React, { createContext, useState } from "react";
import { login, register } from "../API/apiMethods";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [authenticated, setAuthenticated] = useState(false);
return (
<AuthContext.Provider
value={{
setAuthenticated,
authenticated,
login,
register,
}}
>
{children}
</AuthContext.Provider>
);
};
In your form, you can have an extra useEffect to check whether you have logged in successfully. the useEffect will run when the authenticated state has been updated.
const {
login,
setAuthenticated,
authenticated
} = useContext(AuthContext);
const submit = async(e) => {
e.preventDefault();
const authSuccess = await login(email, password);
if (authSuccess) {
console.log("login successfull");
authSetter(true);
}
};
useEffect(() => {
console.log(authenticated);
}, [authenticated]);

Related

How to prevent user for access inner page if not loggged in Using Reactjs

I am working with Reactjs and using nextjs framework,I am working on Login and logout module,I want if any user trying to access inner page without "loggged in" then he should redirect to "index/login" page,How can i do this ? Here is my current code if someone login with correct credentials
const handleSubmit = (e: any) => {
sessionStorage.setItem("email", response.data.email);
const email = sessionStorage.getItem("email");
router.push('/dashboard');
}
I think for checking user authentication it's better to store email or token in cookie and in getServerSideProps check cookie (because you have no access to localStorage in it)
and in getServerSideProps you can access cookie via req.cookies or use cookies package
sth like this :
import Cookies from 'cookies';
export const getServerSideProps = async ({ req, res, locale, ...ctx }) => {
if (req) {
// const posts = axios.get...
const cookies = new Cookies(req, res);
const token = cookies.get('email');
if (!token) {
return {
redirect: {
destination: '/login',
statusCode: 401
}
}
}
return {
props: {
posts,
// your other props
},
};
}
}
and in client code to set cookie you can use js-cookie package
forexample :
import Cookies from 'js-cookie'
const handleSubmit = (e: any) => {
const email = response.data.email
Cookies.set('email', email )
router.push('/dashboard');
}
Well you can do this by holding a context in your whole project or a state or props in specific pages that need permission to view.
So let's assume you have a Dashboard page. You can handle your login in getServerSideProps for better experience and also redirect your user with redirect property before even letting the user to see your page:
export const DashboardPage = (props) => {
console.log(props.data);
return /* ... */;
};
export async function getServerSideProps(context) {
const res = await fetch(`Your Login URL`)
const data = await res.json()
if (!data) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
/* The rest logic of your getServerSideProps */
return {
props: {
data: data,
/* other props */
}
}
}
In this way you should use the logic in every page you have.
Instead you can send the login request from client and set the response to a context value and use that context in every other pages and components.
Context File:
import React, { useState, useEffect } from 'react';
export const UserContext = React.createContext();
export function UserProvider({ children }) {
const [userData, setUserData] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState();
const value = { userData, setUserData, isLoggedIn, setMode };
const handleLogin = async () => {
const res = await fetch(`Your Login URL`);
if(res.status === 200) {
const data = await res.json();
setUserData(data);
setIsLoggedIn(true);
} else {
setIsLoggedIn(false);
}
}
useEffect(() => {
handleLogin();
}, []);
return (
<UserContext.Provider value={value}>
{ children }
</UserContext.Provider>
);
}
And then use the values in your pages, for example Dashboard:
export const DashboardPage = (props) => {
const { isLoggedIn } = useContext(UserContext);
useEffect(() => {
if(isLoggedIn === false) {
router.push('/login');
}
}, [isLoggedIn]);
return /* ... */;
};

Prevent Router from Loading Page Briefly before Redirect

I have a session context for my NextJS application where anyone accessing /app/ directory pages have to go through an authorization check prior to allowing the user to access the page.
While my logic works in redirecting users without proper authentication, it is a bit glitchy because when someone navigate to the URL, /app/profile/ the page briefly loads before being redirected by Router.
I am wondering what is the best way to have this check happen prior to router loading the unauthorized page and redirecting them to the /login/ page.
Here are the steps in the authorization check:
Check is the user object has a property, authorized
Query the server for a session token
if the object from the server request comes back with authorized = false, then redirect user to /login/
Here is the code:
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import axios from 'axios'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const router = useRouter()
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''})
useEffect(()=> {
// Check for populated user state if pages are accessed with the path /app/
if (routerPath.includes("/app/")){
if (user){
if(user.authenticated === undefined){
// Check if user session exists
axios.get('/api/auth/session/')
.then(res => {
const data = res.data;
// Update user state depending on the data returned
setUser(data)
// If user session does not exist, redirect to /login/
if (data.authenticated === false){
router.push('/login/')
}
})
.catch(err => {
console.log(err)
});
}
}
}
}, [])
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
Since user.authenticated isn't defined in the initial user state you can conditionally render null or some loading indicator while user.authenticated is undefined. Once user.authenticated is defined the code should either redirect to "/login" or render the SessionContext.Provider component.
Example:
const AppSession = ({ children }) => {
const router = useRouter();
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''});
...
if (user.authenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return (
<SessionContext.Provider value={{ user, setUser }}>
{children}
</SessionContext.Provider>
);
};
Check out getServerSideProps, redirects in getServerSideProps and this article.
In your client-side, if you export the NextJS function definition named getServerSideProps from a page, NextJS pre-renders the page on each request using the data returned by getServerSideProps.
In other words, you can use getServerSideProps to retrieve and check the user while pre-rendering the page and then choose to redirect instead of render if your condition is not met.
Here is an example.
function Page({ data }) {
// Render data...
}
export async function getServerSideProps(context) {
const { req, res } = context;
try {
// get your user
if (user.authenticated === undefined) {
return {
redirect: {
permanent: false,
destination: `/`,
},
};
}
return {
props: {
// any static props you want to deliver to the component
},
};
} catch (e) {
console.error("uh oh");
return;
}
}
Good luck!

Keep state in react inside context after logged in?

I'm coding a web react app with sign in. In my server side I'm using express, jwt and sending a httpOnly cookie with the token when succesfully log in. When user logs in, I'm trying to keep state in the client (e.x, loggedIn = true) inside of a context, but every time that context is rendered it comes back to default state (undefined). How could i keep that state in memory?
My user route, that works as expected (backend):
users.post('/login',async (req, res) => {
try {
const {userName,userPass} = req.body
const u = await models.User.findOne({
userid: userName
})
if (!u) res.status(404).end()
if (bcrypt.compare(userPass,u.password)) {
// JWT TOKEN
const t = c_auth(u._id)
res.status(200).cookie("session",t,{
httpOnly:true
}).end()
} else {
res.status(404).end()
}
} catch (e) {
console.log({'ERROR':e})
res.status(500).end()
}
})
My user provider that returns true when request is ok (client):
get: async (user,pass) => {
try {
const req = await axios.post('/users/login',{
userName: user,
userPass: pass
})
if (req.status === 200) {
return true
} else {
return false
}
} catch (e) {
console.log({'ERROR':e})
return false
}
}
Login submit function (client):
import {useAuth} from '../../../../contexts/AuthContext.js'
const {setLoggedIn} = useAuth()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
// ^---> Trying to set loggedIn state to true in context
window.location.replace('/')
} else {
alert(`Incorrect.`)
}
}
Auth context:
const AuthContext = createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider ({children}) {
const [loggedIn,setLoggedIn] = useState()
console.log(loggedIn)
// ^---> Getting true after login,
// undefined (default useState) after re-render
const value = {
loggedIn,
setLoggedIn
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
App.js:
import {AuthProvider} from './contexts/AuthContext'
function App() {
return (
<AuthProvider>
<div className="App">
<Navigation/>
<main>
<Router>
<Routes>
...Routes
</Routes>
</Router>
</main>
</div>
</AuthProvider>
)
}
I suppose that react memo could be the solution, but I don't understand quite well how it works. Also, is it correct not to use the setLoggedIn in the AuthContext itself? I tried to call the login or sign up function (second piece of code) from the AuthContext, but can't set state since its unmounted. Would need to do that inside a useEffect and that's not what I'm looking for since I wouldn`t be able to export that function. All help is appreciated.
EDIT: fixed
The problem solved after changing window.location.replace to the useNavigate hook from react-router-dom, causing a refresh:
import {useAuth} from '../../../../contexts/AuthContext.js'
import {useNavigate} from 'react-router-dom'
const {setLoggedIn} = useAuth()
const navigate = useNavigate()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
navigate('/')
} else {
alert(`Incorrect.`)
}
}
Also in my navbar I was using <a href> tags instead of <Link to> from 'react-router-dom'. That fixes the problem when I go to a different page from the navbar, so it doesn't 'refresh'.

React: Adding token to localStorage in loginHandler function via State hook not working

I've created an AuthContextProvider in my React app for login/logout-functionality:
import React, { useState } from "react";
import axios from "axios";
import { api } from "../api";
const AuthContext = React.createContext({
token: "",
isLoggedIn: false,
login: () => {},
logout: () => {},
});
export const AuthContextProvider = (props) => {
const initialToken = localStorage.getItem("token");
const [token, setToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const loginHandler = (email, password) => {
return axios
.post(api.auth.login, {
email,
password,
})
.then((res) => {
setToken(res.data.key);
localStorage.setItem("token", token)
// localStorage.setItem("token", res.data.key)
});
};
const logoutHandler = () => {
setToken(null);
localStorage.removeItem("token")
};
const contextValue = {
token: token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
In my loginHandler I tried to add the token received from by backend to localStorage via localStorage.setItem("token", token) but this always gives me null for the value of the token in localStorage. When I add the token via localStorage.setItem("token", res.data.key) I manage to get the actual token in localStorage. I don't understand why the first approach is not working, since I set token properly via setToken(res.data.key) just one line above.
Any comments would be highly appreciated
The problem is that setState() is asynchronous. React does not guarantee that the state changes are applied immediately. If you want to perform an action immediately after setting state on a state variable, you need to pass a callback function to the setState function. Since in a functional component no such callback is allowed with useState hook, you can use the useEffect hook to achieve it.
Here is complete explanation how to do that.

Unnecessary rerenders using Apollo + useContext for Authentication (SSR)

I'm experiencing rerenders seemingly as a result of useQuery being called when I don't want it to be.
First load, isAuthenticated's state is instantiated via a function that makes a query to the server to get the userId based on the attached header.
If a valid header is present, isAuthenticated is set to true and therefore useMemo is called (as isAuthenticated is a dependency). useMemo calls setUser, which puts the userId and role into the context, to be passed down through the AuthProvider (in _app, wrapping everything but the ApolloProvider).
I use the userId in the Navbar (via useContext) to get the avatar (and other things in the rest of the app). What I've noticed is that any time I click on something (for example, a separate query filtering products), the CURRENT_USER query in _app is called. And this manifests as the Navbar losing the useId data passed to it for a second, and then reverts back. So, everytime I click on things, my Navbar is rerendering. I don't understand why the CURRENT_USER query keeps being called...
For context, I use the same setup in another app that does not have SSR without issue.
auth.context.js
import React, { useState, useEffect, useMemo} from "react";
import { CURRENT_USER } from "graphql/query/customer.query";
import { gql, useQuery } from "#apollo/client";
import { isBrowser } from "components/helpers/isBrowser";
export const AuthContext = React.createContext({});
export const AuthProvider = ({ children }) => {
const { data, error, loading } = useQuery(CURRENT_USER, {
onCompleted: () => {
console.log("called query", data);
},
ssr: true,
});
const isValidToken = () => {
if (isBrowser) {
const token = localStorage.getItem("token");
if (error) {
return false;
}
if (!loading && !data) {
return false;
}
if (token && data?.currentUser) {
console.log(data.currentUser);
return true;
}
if (!token) {
return false;
}
}
};
const [isAuthenticated, makeAuthenticated] = useState(isValidToken());
const [user, setUser] = useState({});
function authenticate() {
console.log(isAuthenticated);
makeAuthenticated(isValidToken());
}
useEffect(() => {
console.log("attempted to run");
if (data && isAuthenticated) {
setUser(data.currentUser);
console.log("user", user);
}
}, [isAuthenticated]);
function signout() {
makeAuthenticated(false);
localStorage.removeItem("token");
}
return (
<AuthContext.Provider
value={{
isAuthenticated,
authenticate,
signout,
user,
}}
>
{children}
</AuthContext.Provider>
);
};
"#apollo/client": "^3.0.0-beta.44",
"next": "^9.3.6",
"react": "^16.13.1",

Categories