Search code examples
reactjsnext.jsjwtserver-side-renderingreact-context

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


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 ?


Solution

  • 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