How to persist user state in Next.js app with useContext - reactjs

I'm working on a web application with react and Next.js and I also have a Node.js API separated as a back-end.
I have a login form where I send the data to the API to recover JWT, when I do that, everything works fine, but after redirecting the user to the protected route "dashboard", or after a refresh the user context gets lost.
Here is the protected route :
import React, {useContext, useEffect} from 'react'
import { useRouter } from "next/router";
import { Context } from '../../context/context';
export default function DashboardIndexView() {
const router = useRouter();
const {isUserAuthenticated, setUserToken, userToken} = useContext(Context);
useEffect(() => {
isUserAuthenticated()
? router.push("/dashboard")
: router.push("/authentication/admin");
}, []);
return (
<>
<h1>Dashboard index view</h1>
</>
)
}
and here is the context file :
import React, {createContext, useEffect, useState} from 'react'
import { useRouter } from "next/router";
import axios from 'axios'
export const Context = createContext(null)
const devURL = "http://localhost:4444/api/v1/"
export const ContextProvider = ({children}) => {
const router = useRouter()
const [user, setUser] = useState()
const [userToken, setUserToken] = useState()
const [loading, setLoading] = useState(false)
const [successMessage, setSuccessMessage] = useState("")
const [errorMessage, setErrorMessage] = useState("")
const Login = (em,pass) => {
setLoading(true)
axios.post(devURL+"authentication/login", {
email : em,
password : pass
})
.then((res)=>{
setSuccessMessage(res.data.message)
setErrorMessage(null)
setUser(res.data.user)
setUserToken(res.data.token)
localStorage.setItem('userToken', res.data.token)
localStorage.setItem('user', res.data.user)
setLoading(false)
})
.catch((err)=>{
setErrorMessage(err.response.data.message)
setSuccessMessage(null)
setLoading(false)
})
}
const Logout = () => {
setUserToken()
setUser()
localStorage.setItem('userToken', null)
localStorage.setItem('user', null)
router.push('/authentication/admin')
}
const isUserAuthenticated = () => !!userToken
return (
<Context.Provider value={{
Login,
user,
loading,
userToken,
setUserToken,
Logout,
successMessage,
setSuccessMessage,
setErrorMessage,
isUserAuthenticated,
errorMessage}}>
{children}
</Context.Provider>
)
}
How can I keep the user on the dashboard page even when a refresh happens ?

It's normal for useContext() to lose its value on a page refresh. Contexts don't persist any data, they simply share the data between components. In, Next.js, it can work between pages because Next.js handles navigation on the client side. But as you've noticed, as soon as you refresh, the app is mounted from scratch and this time the context never gets the value of the JWT because the JWT was never sent on this new instance of your app.
The solution, at a high-level, is to store the JWT somewhere (localStorage or cookie) and inject the value in your Context.Provider. You're already setting the values in localStorage now you just need a useEffect that will read them and add them to the context:
useEffect(() => {
setUser(localStorage.get('user'));
setUserToken(localStorage.get('userToken'));
}, []);
But the real solution, in my opinion, is to use https://next-auth.js.org/ instead. It handles security concerns and is a well-known library for Next.js

Related

setState not working inside useEffect hook and token is showing null everytime

I am building a wrapper which will see if token is present or not in the localstorage then
it will render the component otherwise redirect the user to the login page.
The auth and token state are not getting changed...
import Home from "./Home";
import { Redirect } from "react-router-dom";
import { useEffect, useState } from "react";
function Protected_home() {
const[token , setToken] = useState(null)
const [auth,setAuth] = useState(false)
useEffect(()=>{
setToken(localStorage.getItem("noty__auth__token")) //not working
console.log("token fetched") // getting a log the token
},[])
useEffect(()=>{
setAuth(true) // setting auth to true
},[token])
useEffect(()=>{
alert(token) // getting null
},[auth])
// conditional rendering of the components
if(auth){
return <Home/>
}else{
return <Redirect to={{pathname:"/"}}/>
}
}
export default Protected_home
you should setItem in localsotorage and then use getItem method if you've done setting the value check your browser storage to see is there any data or not.
check this thing out too
useEffect(()=>{
const data = localStorage.getItem("noty__auth__token");
if(data) setToken(data)
},[])
Trying giving the conditions before setting state. As on first render every useEffect will be called unlike of dependency array.
import "./styles.css";
import { Redirect } from "react-router-dom";
import { useEffect, useState } from "react";
export default function App() {
const [token, setToken] = useState(null);
const [auth, setAuth] = useState(false);
useEffect(() => {
console.log("chek");
const data = localStorage.getItem("noty__auth__token");
if (data) {
setToken(data); //not working
console.log("token fetched");
} // getting a log the token
}, []);
useEffect(() => {
console.log("auth");
if (token) {
setAuth(true);
} // setting auth to true
}, [token]);
useEffect(() => {
console.log("alert");
//alert(token); // getting null
}, [auth]);
// conditional rendering of the components
if (auth) {
return <h1>welcomehome</h1>;
} else {
return <h2>hey</h2>;
}
}

Route to page inside useEffect

I'm trying to produce a minimal example of routing to login if no session is found. Here is my code from _app.js inside pages folder :
function MyApp({ Component, pageProps }) {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(() => {
const session = document.cookie.includes("session_active=true")
if (session) {
fetch("/api/user")
.then(u => u.json().then(setUser))
} else {
const redirectURI = router.pathname
const url = {pathname: "/login", query: {"redirect_uri": redirectURI}}
router.push(url)
}
}, [])
if (!user) return Loading()
return (<div>User {user.name} {user.surname}</div>)
}
My login is inside pages/login.js with this content :
const Login = () => (<div>Login page</div>)
export default Login
However it's stuck on the loading page even though I don't have the session. Am I misusing the router ?
The URL is changed properly to /login?redirect_uri=%2Ffoo but the content is not the one from my Login
Below is a stackblitz reproduction: https://stackblitz.com/edit/github-supacx-rpl5rm
I see the problem, You are preventing the app to load.
You are not changing user's state in case there is no session_active cookie.
You are trying to render the only loading component instead of the next App.
if (!user) return Loading()
Solution:
Let the app render
render the loading component inside the return statement of the app component
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
export default function App({ Component, pageProps }) {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(() => {
const session = document.cookie.includes('session_active=true')
if (session) {
fetch('/api/user').then((u) => u.json().then(setUser))
} else {
setUser(true) // set to true.
const redirectURI = router.pathname
const url = { pathname: '/login', query: { redirect_uri: redirectURI } }
router.push(url)
}
}, [])
return (
<>
{!user && <div>loading</div>}
<Component {...pageProps} />
</>
)
}
I am not sure which approach you will use to pass user info to all components. My suggestion would be to create a context for authentication and wrap the app with it. Then handle the user session and redirection in the context.

aws-amplify-react and nextjs breaks my user context

I cant figure out why but when I use cognito with my own custom user context everything works just fine but as soon as I use withAuthenticator higher order component it breaks my user context and I cant for the life of me figure out why, or even how to fix it. Ill post my user context file below for reference and tell you where it breaks.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
//TODO must redo cognito from scratch and will probably be able to keep this user context untouched
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser().then(x=>setUser(x)).catch((err)=>setUser(null))
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false)
Auth.signIn(username, password)
.then( x => {
setUser(x)
console.log('Welcome: ' + x.challengeParam.userAttributes.email)
setUserEmail(x.challengeParam.userAttributes.email)
setSignInError(false)
})
.catch((err)=>{
console.log(err.code)
if(err.code === 'UserNotFoundException' || 'NotAuthorizedException'){
err.message = 'Invalid username or password'
setSignInError(true)
console.log(err.message)
}
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
return x
})
}
const signup = (username, email, password) => {
Auth.signUp({ username, password, attributes: { email } })
.then( x => {
setUser(x)
return x
})
.catch((err)=>{
if(err.code){
err.message = 'Your Username or Password was incorrect'
}
throw err
})
}
const vals = useMemo( () => ({user, login, logout, signup, handleSignInError, userEmail, signInError}), [user, userEmail, signInError])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}
Under the login function it now returns user not found after I wrap a component and npm i aws-amplify-react. The funny thing is when I uninstall it I still get the same error and cant go back without fully removing amplify and going through a complete amplify init again. Even more confusing, My app is hosted on vercel and that breaks after I attempt to do this on my local machine. If im not missing something there and my app does break in the cloud even though I dont push my modified code then im guessing cognito is getting something in the cloud when I attempt this on my local machine and then screwing up my untouched copy on vercel????? Since then Ive also tried using next-auth which makes me think I should just stick to front end work or find a better solution? any help would be appreciated. Ill revert to my old setup and rebuild my cognito and amplify from scratch just to get it going again.
You need to call Cognito configure prior to calling your auth provider. Place it before you define your auth provider or context.
Auth.configure({...your_config})
const UserContext = () => {};
I also use a auth hook with my context that removes the need for a HOC.
import { useContext } from 'react';
export const useAuth = () => useContext(UserContext);
// use it in components and pages
const user = useAuth();
Ensure that your configuration is using all of the proper types. If you don't, it sometimes fails silently. For example ENV files are always passed as strings so some options must be cast to the proper type like cookie expires
{
authenticationFlowType: 'USER_SRP_AUTH',
cookieStorage: {
...other settings
expires: Number(process.env.NEXT_PUBLIC_COGNITO_COOKIE_EXPIRES),
}
};
You will also need to call Auth.configure on every page that you need access to Congito auth inside of getStaticPaths, getStaticProps, and getServerSideProps. This is because they are independently called from your app during build or on a server.
Auth.configure({...your_config})
const getStaticProps = () => {};
const getStaticPaths = () => {};
const getServerSideProps = () => {};
If you can use it, their hosted UI is pretty good.
Lastly, AWS has a few libraries for Amplify and I use #aws-amplify/auth - I don't know if this makes a difference.
I added the config file to my _app.js and set ssr: true for ssr authentication
import Amplify from 'aws-amplify'
import config from '../src/aws-exports'
Amplify.configure({...config, ssr: true})
Here is my working user context. I removed the signup function and will add it later once i work on it and test it.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
const [sub, setSub] = useState(null)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser()
.then(x=>{
setUser(x.username)
setUserEmail(x.attributes.email)
setSub(x.attributes.sub)
})
.catch((err)=>{
console.log(err)
setUser(null)
})
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false);
Auth.signIn(username, password)
.then((x) => {
setUser(x.username)
setSignInError(false)
console.log(x)
})
.catch((err)=>{
console.log(err)
setSignInError(true)
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
setSub(null)
})
}
}
const vals = useMemo( () => ({user, sub, login, logout, handleSignInError, userEmail, signInError}), [user, userEmail, signInError, sub])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}

React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application

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

asynchronous context with useEffect in React

im trying to create an api request with the header value, that is received from a context component. However, as soon as the page component is loaded, it throws an Cannot read property '_id' of null exception. Is there a way to run the useEffect function, as soon as the context is loaded?
main component:
import React, { useState, useEffect, useContext } from "react";
import "./overview.scss";
/* COMPONENTS */;
import axios from 'axios';
import { GlobalContext } from '../../components/context/global';
const Overview = () => {
const [bookings, setBookings] = useState([]);
const [loaded, setLoaded] = useState(false);
const [user, setUser] = useContext(GlobalContext);
useEffect(() => {
axios
.get(`/api/v1/bookings/user/${user._id}`)
.then(res => setBookings(res.data))
.catch(err => console.log(err))
.finally(() => setLoaded(true));
}, [user]);
context component:
import React, {useState, useEffect, createContext} from 'react';
import jwt from 'jsonwebtoken';
/* GLOBAL VARIABLES (CLIENT) */
export const GlobalContext = createContext();
export const GlobalProvider = props => {
/* ENVIRONMENT API URL */
const [user, setUser] = useState([]);
useEffect(() => {
const getSession = async () => {
const user = await sessionStorage.getItem('authorization');
setUser(jwt.decode(user));
}
getSession();
}, [])
return (
<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>
);
};
The issue here is useEffect is running on mount, and you don't have a user yet. You just need to protect against this scenario
useEffect(() => {
if (!user) return;
// use user._id
},[user])
Naturally, when the Context fetches the user it should force a re-render of your component, and naturally useEffect should re-run as the dependency has changed.
put a condition before rendering you GlobalProvider, for example:
return (
{user.length&&<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>}
);
If user is not an array just use this
return (
{user&&<GlobalContext.Provider value={[user, setUser]}>
{props.children}
</GlobalContext.Provider>}
);

Resources