reactjsexpresslocal-storagetokenexpress-jwt

React Failing to get the localStorage init value


Hello everyone im working on a mern app and got curious how to implement the jwt token authentication, it works fine but i got a small problem in my front, the token set successfully on the localstorage after the user login, but when i try to fetch the values using axios while sending the token in the headers, the token value is always null on the first render, i had to refresh the page so the headers get access to the token from the local storage This is the useLogin component, the token is immediately set on the localStorage

const login = async (email, password) => {
const user = { email, password };
setError(null);
try {
  const response = await axios.post(
    "http://localhost:4000/api/user/login",
    user
  );
  const result = response.data;

  console.log(response);

  if (response.statusText === "OK") {
    setError(null);
    setIsLoading(false);
    // save the token in local storage
    localStorage.setItem("user", JSON.stringify(result));
    // update the dispatch
    dispatch({ type: "LOGIN", payload: result });
  }
} catch (error) {
  const response = error.response.data;
  setError(response.message);

  //console.log(error);
}

};

Localstorage token

This is my authContext where im trying to get the localStorage Value


export const AuthContext = createContext();

export const authReducer = (state, action) => {
  switch (action.type) {
    case "LOGIN":
      return { user: action.payload.user };
    case "LOGOUT":
      return { user: null };

    default:
      return state;
  }
};

export const AuthContextProvider = ({ children }) => {
  const initialState = JSON.parse(localStorage.getItem("user")) || null;

  const [state, dispatch] = useReducer(authReducer, { user: initialState });
  // useEffect(() => {
  //   //check if there is a token in local storage
  //   const user = JSON.parse(localStorage.getItem("user"));
  //   console.log(user);

  //   if (user !== undefined && user !== null) {
  //     dispatch({ type: "LOGIN", payload: { user } });
  //   }
  // }, []);

  console.log("authContext state", state);

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

i tried using useEffect and it resulted in the same problem, the getItem("user") always null on the fist render, and works fine after i refresh the page

finally the home component where im trying to fetch the data

the user.token variable always undefined on the first render but works fine when i refresh

import axios from "axios";
import { useWorkoutContext } from "../hooks/useWorkoutContext";
import { useAuthContext } from "../hooks/useAuthContext";
// component
import WorkoutDetails from "../components/workoutDetails";
import WorkoutForm from "../components/workoutForm";

const Home = () => {
  // const [workouts, setWorkouts] = useState([]);
  const { workouts, dispatch } = useWorkoutContext();
  const [error, setError] = useState(null);
  const { user } = useAuthContext();

  useEffect(() => {
    const fetchWorkouts = async () => {
      console.log(user.token);
      try {
        if (user && user.token) {
          const response = await axios.get(
            "http://localhost:4000/api/workouts",
            {
              headers: {
                Authorization: `Bearer ${user.token}`,
              },
            }
          );
          const result = response.data;
          if (response.status === 200) {
            //  console.log(result);
            // setWorkouts(result);
            dispatch({ type: "SET_WORKOUTS", payload: result });
          }
        }
      } catch (error) {
        console.error("Error fetching workouts:", error);
        setError("Error fetching workouts. Please check your network.");
      }
    };
    //console.log(user);
    if (user) {
      fetchWorkouts();
    }
  }, [dispatch, user]);
  return (
    <div className="home">
      <div className="workouts">
        {workouts &&
          workouts.map((workout) => {
            return <WorkoutDetails key={workout._id} workout={workout} />;
          })}
        {error && <p>{error}</p>}
      </div>
      <WorkoutForm />
    </div>
  );
};

export default Home;


This is my Back requireAuth express code

import { userModel } from "../models/userModel.js";
const requireAuth = async (req, res, next) => {
  // verify authentication
  // we get authorization from the headers
  const { authorization } = req.headers;

  console.log(authorization);

  if (!authorization) {
    res.status(401).json({ error: "authorization required" });
  }
  // authorization is a string that contains the token
  let token;
  if (authorization) {
    token = authorization.split(" ")[1];
    console.log("Token is " + token);
  }

  try {
    // returns the id of the user that is in the token
    const { _id } = jwt.verify(token, process.env.SECRET);
    console.log("id is " + _id);
    req.user = await userModel.findOne({ _id }).select("_id");
    next();
  } catch (error) {
    console.log(error);
    res.status(401).json({ error: "request is not authorized" });
  }
};
export default requireAuth;

im stuck on this for 2 days now :'((


Solution

  • That is happening because your response consists of email, token and user object. You are storing the whole response in the local storage so you store an object consisting of email, token and user. But when you handle the LOGIN event you store just the user object from the response there and the other two keys (email and token are discarded).

    On the first render after the LOGIN event is dispatched you try to access the token key but it is not in the user object - it was outside of it and you haven't saved it in the reducer.

    When you refresh the page you parse the content from the local storage and in the initial state you have the whole response including the email, token and user object. And when you use the parsed object and ask for the token value it is now accessible.

    You need to do the following change:

    export const authReducer = (state, action) => {
      switch (action.type) {
        case "LOGIN":
          return { user: action.payload}; // save the whole payload instead of just the user object (action.payload.user)
        case "LOGOUT":
          return { user: null };
    
        default:
          return state;
      }
    };