Skip to content
Advertisement

Persist the login state in Next js using localstorage

I’m writing an authentication application in Next Js (v12.2.5). The application also uses React (v18.2.0).

The problem is with persisting the authentication state. When the browser is refreshed, the login session is killed. Why is this happening even though I am getting and setting the token in the local storage. I would like to persist the login session to survive browser refresh and only kill the session when the user logs out.

Application flow

Users authenticates through a login form which calls a Spring API to fetch credentials from a mySQL database. Everything works as expected. The user is able to login, conditional rendering and route protection all function as expected.

Persisting the session relies on the localstorage API to store the JWT token once the user logs in. The Chrome browser console shows that the token is successfully set and stored throughout the authentication process. The get method for getting the initial token also seems to work.

Background

There are several questions on SFO that cover this topic but most seem to cover the use of cookies like this example. This question covers localstorage, but simply says to wrap the token get method is useEffect which doesn’t address the actual questions and problems I’m having.

This example also covers localstorage but takes a different approach, using useReducer where my approach is trying to use use Effect. I’m open to restructure my whole application to use useReducer if this is the correct way, but first I want to make sure I understand if I’m taking the right approach.

I also suspect there is a difference between persisting the user state using React and Next. From researching, the difference seems to be in the way Next also includes SSR which may explain why I’m not able to persist the state in Next?

Application code

auth-context.js

const AuthContext = React.createContext({
  token: '',
  admintoken: '',
  isLoggedIn: false,
  isAdmin: false,
  login: (token) => { },
  adminAccess: (admintoken) => { },
  logout: () => { },
});

export const AuthContextProvider = (props) => {
  useEffect(()=> {
  if(typeof window !== 'undefined') {
    console.log('You are on the browser');
      initialToken = localStorage.getItem('token');
      console.log("InitialToken set "+ initialToken);
  
  } else {
    initialToken = localStorage.getItem('token');
    console.log('You are on the server and token is ' + initialToken);
  }
},[AuthContext])



  const [token, setToken] = useState(initialToken);
  const [admintoken, setAdminToken] = useState(initialToken);

  const userIsLoggedIn = !!token;
  const userHasAdmin = !!admintoken;


  const loginHandler = (token) => {
    setToken(token);
    localStorage.setItem('token', token);
    console.log("token stored " + token);
    };


  const logoutHandler = () => {
    setToken(null);
    localStorage.removeItem('token');
  };

  const adminTokenHandler = (admintoken) => {
    setAdminToken(admintoken);
  }

  const contextValue = {
    token: token,
    admintoken: admintoken,
    isAdmin: userHasAdmin,
    isLoggedIn: userIsLoggedIn,
    adminAccess: adminTokenHandler,
    login: loginHandler,
    logout: logoutHandler,
  };


  return (
    <AuthContext.Provider value={contextValue}>
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

ProtectRoute.js

const ProtectRoute = ({ children }) => {
  const authCtx = useContext(AuthContext);
const isLoggedIn = authCtx.isLoggedIn;

if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname == '/') {

      return <HomePage />;
    } else {
      if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname !== '/auth') {
        return <RestrictedSection />;
      } 
      else {
      console.log("User logged in");
      // return <RestrictedSection />;
   return children;
    } 
  }

}

export default ProtectRoute;

Authform.js (login page)

const AuthForm = () => {
const emailInputRef = useRef();
  const passwordInputRef = useRef();
  const [isLoading, setIsLoading] = useState(false);
  const [isAdmin, setIsAdmin] = useState(false);
  const router = useRouter();

  const authCtx = useContext(AuthContext);


  const submitHandler = (event) => {
  
    event.preventDefault();

    const enteredEmail = emailInputRef.current.value;
    const enteredPassword = passwordInputRef.current.value;

      var myHeaders = new Headers();
      myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
      
      var urlencoded = new URLSearchParams();
      urlencoded.append("username", enteredEmail);
      urlencoded.append("password", enteredPassword);

      var requestOptions = {
        method: 'POST',
        headers: myHeaders,
        body: urlencoded,
        redirect: 'follow'
      };

      fetch(API_LOGIN_URL, requestOptions)
        .then(async (res) => {
          setIsLoading(false);
          if (res.ok) {
            return res.json();
          } else {
            const data = await res.json();
            let errorMessage = 'Authentication failed!';
            throw new Error(errorMessage);
          }
        })
        .then((data)=> {
          authCtx.login(data.access_token);
          router.replace('/');
          

          const processedData = JSON.stringify(data);
          console.log("Admin status "+ processedData);
         for(let i = 0; i < processedData.length; i++) {
                if(processedData.includes("ROLE_SUPER_ADMIN")) {
                console.log("Found Admin"); 
                authCtx.adminAccess(true);
              } 
              if(processedData.includes("ROLE_USER")) {
                console.log("Found User");
                break;
              }
              else {
                console.log("Not Found");
              }
        }})
        .catch((err) => {
          alert(err.message);
        });
    
  };

  return (
    <section className={classes.auth}>
      <h1>Login</h1>
      <form onSubmit={submitHandler}>
        <div className={classes.control}>
          <label htmlFor='email'>Your Email</label>
          <input type='email' id='email' required ref={emailInputRef} />
        </div>
        <div className={classes.control}>
          <label htmlFor='password'>Your Password</label>
          <input type='password' id='password' required ref={passwordInputRef} />
        </div>
        <div className={classes.actions}>
          {!isLoading && <button>Login</button>}
          {isLoading && <p>Sending request</p>}
        </div>
      </form>
    </section>
  );
};

export default AuthForm;

Advertisement

Answer

Issue 1
From your code in auth-context.js, you are calling useEffect inside a condition.

  if(typeof window !== 'undefined') {
    console.log('You are on the browser');
    useEffect(()=> {
      initialToken = localStorage.getItem('token');
      console.log("InitialToken set "+ initialToken);
    })
    
  } else {
    useEffect(()=> {
    initialToken = localStorage.getItem('token');
    console.log('You are on the server and token is ' + initialToken);
  })
  }

You SHOULD NOT call your useEffect(or any other hook) inside conditions, loops and nested functions.
Doc reference: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level.

Consider moving your conditions code inside the hook.

useEffect(()=> {
 if(condition)
  {run your localstorage related logic here...}
  })

Issue 2
I think you should consider adding a dependency array to your useEffect hook because getting your token on every rerender seems quite expensive.

useEffect(()=> {
 if(condition)
  {run your localstorage related logic here...}
  },[])

Still, its just a suggestion, as I don’t know your code in much depth.

Issue 3
The initial token is not getting set in the use effect.
Kindly add setToken(initialToken) in the useEffect after initial token assignment.

initialToken = localStorage.getItem('token');
setToken(initialToken);

The main issue is with you trying to run serverside code on the fronted:

  useEffect(()=> {
  if(typeof window !== 'undefined') {
    console.log('You are on the browser');
      initialToken = localStorage.getItem('token');
      console.log("InitialToken set "+ initialToken);
  
  } else {
    initialToken = localStorage.getItem('token');
    console.log('You are on the server and token is ' + initialToken);
  }
},[AuthContext])

The above part of the code will always run on the front end(so you don’t need the if part). If you want to clear your concepts on what part of the code will work on the server and what part will run on the client, kindly refer to these documentations:

SSR: https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props
SSG: https://nextjs.org/docs/basic-features/data-fetching/get-static-props
ISR: https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration

Advertisement