skip to Main Content

My react application has several components whose UI will show up if the user is logged in or not. To share that state across my entire application, I made a context file called AuthProvider:

import { createContext, useState, useContext, useEffect } from 'react';
import jwtDecode from 'jwt-decode';
import PropTypes from 'prop-types';

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [userName, setUserName] = useState(null); 

  useEffect(() => {
    const token = localStorage.getItem('token');

    if (token) {
      const decodedToken = jwtDecode(token);
      const user = decodedToken.user; 
      setUser(user);
      setUserName(user.user_name); 
    }
  }, []); 

  return (
    <AuthContext.Provider
      value={{
        user,
        userName, 
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

AuthProvider.propTypes = {
  children: PropTypes.node.isRequired,
};

export const useAuth = () => {
  return useContext(AuthContext);
};

the main purpose of this context file is to check if the user is logged in, and if they are, send that information to all other components that need that information. If there are no tokens, that means the user is not logged in yet, and there’s where my Login page comes in.

This is the Login page code:

import  { useState, useEffect } from 'react';
import axios from 'axios';
import jwtDecode from 'jwt-decode'; 

function Login() {
    const [user, setUser] = useState(null);

    useEffect(() => {
        const token = localStorage.getItem('token');

        if (token){
            const decodedToken = jwtDecode(token);
            setUser(decodedToken.user);
        }
    }, [])

    const handleSubmit = async (e) => {
        e.preventDefault();
    
        const email = e.target.email.value;
        const password = e.target.password.value;
        try {
            const response = await axios.post('http://localhost:5000/auth/login', {email, password});
            const token = response.data.token;
            localStorage.setItem('token', token);
            const decodedToken = jwtDecode(token);
            setUser(decodedToken.user);
            // if i remove this, I have to manually reload the page for the login to kick in
            // window.location.reload();
        } catch (error) {         
            console.log(error.message);
        }
    }

    return (
        <div>
            {user ? (
                <div>
                    <h1>Welcome back!</h1>
                </div>
            ): (
                <form onSubmit={handleSubmit}>
                    <div className="mt-4"> 
                        <div>
                            <label>Email:</label>
                            <input type="email" name='email' required placeholder="[email protected]" /> 
                        </div>
                        <div>
                            <label >Password:</label>
                            <input type="password" name='password' required placeholder="****" /> 
                        </div>
                    </div>
                    <button type="submit" >Login</button>
                </form>
            )}
        </div>
    );
}

export default Login;

as soon as the login page is rendered, it will check if there is a token in the browser. if there is, it will decode that token and set its value to the state "user." in the UI, if state "user" is true, it means the user is already logged in and thefore will not render the login form itself.
If there’s no token, the user will fill the form and submit it, which will trigger the function handleSubmit. In there, we will make an axios post request to my api, get the token of that user and set it to the browser storage, thus effectively making the user logged in as far as the AuthProvider context is concerned.

the problem is, after successfully logging in, if the page is not reloaded, it will still treat as if the user is not logged in. For example, in the Navbar component, certain buttons and icons in the UI will only show if the user is logged in. Only after I manually reload the page, it will show them. I was using the "window.location.reload();" to automatically reload the page after submitting the user login form, but for several reasons, I don’t want to use that anymore.

why is this happening? I put the context file on top of of the hierarchy, above the the pages and components to make sure that every time a component or page get rendered, the context AuthProvider will check if there is a token in the browser storage and distribute that data throught the application, which after doing the login, there should be one.

this is the App.jsx file:

function App() {
  return (
    <div>
      <BrowserRouter>
        <AuthProvider>
          <Navbar />
          <PageRoutes />              
        </AuthProvider>
      </BrowserRouter>
    </div>
  );
}

I’m not sure if the PageRoutes component is relevant for this problem, but just in case:

import { Routes, Route, useLocation } from "react-router-dom";
import Home from '../pages/Home';
import Login from '../pages/Login';
import Register from "../pages/Register";
import Cart from "../pages/Cart";
import About from "../pages/About";

const PageRoutes = () => {
    const location = useLocation();

    return (
        <Routes location={location} key={location.pathname}>
            <Route path="/" index element={<Home />} />
                <Route path="login" element={<Login />} />
                <Route path="register" element={<Register />} />
                <Route path="cart" element={<Cart />} />
                <Route path="about" element={<About />} />
        </Routes>
    )
}

export default PageRoutes;

2

Answers


  1. Instead of updating the state that’s local to Login, update the state in the AuthProvider. Also, it looks like userName is just derived from user so i recommend you don’t split it into a separate state

    export const AuthProvider = ({ children }) => {
      const [user, setUser] = useState(null);
      // ...
    
      return (
        <AuthContext.Provider value={{
          user,
          userName: user?.user_name,
          setUser
        }}>
          {children}
        </AuthContext.Provider>
      );
    }
    
    function Login() {
      const { user, setUser } = useContext(AuthContext);
      
      // ... the rest can stay the same
    })
    

    P.S, you should memoize the value you pass into the provider, otherwise you’ll be making a new object on every render and forcing all consumers of the context to rerender even if nothing changed.

    const value = useMemo(() => {
      return {
        user,
        userName: user?.user_name,
        setUser
      };
    }, [user])
    
    return (
      <AuthContext.Provider value={value}>
        {children}
      </AuthContext.Provider>
    );
    
    Login or Signup to reply.
  2. You’ll need to expose some way of mutating the context’s state in order to have it update and propagate to others.

    It’s also a good idea to encapsulate the persistence method (ie localStorage) within the context provider. This makes it opaque to consumers and lets you more easily change it if required.

    For example, in AuthProvider

    const [user, setUser] = useState(null);
    
    // encapsulate persistence
    const setToken = (token) => {
      const { user } = jwtDecode(token);
      setUser(user);
    
      localStorage.setItem("token", token);
    };
    
    // encapsulate authentication mechanism
    const login = async (email, password) => {
      const {
        data: { token },
      } = await axios.post(
        "/auth/login",
        { email, password },
        {
          baseURL: process.env.REACT_APP_API_BASE_URL, // use env vars
        },
      );
      setToken(token);
    };
    
    const logout = () => {
      setUser(null);
      setUserName(null);
      localStorage.removeItem("token");
    };
    
    useEffect(() => {
      const token = localStorage.getItem("token");
      if (token) {
        setToken(token);
      }
    }, []);
    
    return (
      <AuthContext.Provider
        value={{
          user,
          userName: user?.user_name,
          login,
          logout,
        }}
      >
        {children}
      </AuthContext.Provider>
    );
    

    Then in components like Login you can use these context methods

    const { login } = useAuth();
    
    const handleSubmit = async (e) => {
      e.preventDefault();
    
      const email = e.target.email.value;
      const password = e.target.password.value;
      try {
        await login(email, password);
        // then navigate or whatever
      } catch (error) {
        console.log(error.message);
      }
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search