How to use useEffect and the Context API to check if a user is logged in and protect a route?

Viewed 4200

I am trying to protect routes based on whether a user is logged in or not but I cannot get this to work properly since it seems that the information stored in my context provider is not available on the initial component load.

I am checking whether the user is authenticated within my App.js file by making a request to my node server through the useEffect hook. It tries to store this info within the context api which it successfully does but it appears that rendering other components will not wait for the context api to "catch up" or load first.

I am sure there is something simple I am missing or maybe I am using bad convention with checking if a user is authenticated. Any help would be appreciated!

App.js

import React, { useState, useEffect } from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Axios from 'axios';
import Header from './components/layout/Header';
import Home from './components/pages/Home';
import HiddenPage from './components/pages/HiddenPage';
import Login from './components/auth/Login';
import Register from './components/auth/Register';
import UserContext from './context/UserContext';
import ProtectedRoute from './components/auth/ProtectedRoute';

import './style.scss';

export default function App() {
  const [userData, setUserData] = useState({
    token: undefined,
    user: undefined,
  });

  useEffect(() => {
    const checkLoggedIn = async () => {
      let token = localStorage.getItem('auth-token');
      if (token === null) {
        localStorage.setItem('auth-token', '');
        token = '';
      }
      const tokenResponse = await Axios.post(
        'http://localhost:5000/users/tokenIsValid',
        null,
        { headers: { 'x-auth-token': token } }
      );
      if (tokenResponse.data) {
        const userResponse = await Axios.get('http://localhost:5000/users/', {
          headers: { 'x-auth-token': token },
        });
        setUserData({
          token,
          user: userResponse.data,
        });
      }
    };

    checkLoggedIn();
  }, []);

  return (
    <>
      <BrowserRouter>
        <UserContext.Provider value={{ userData, setUserData }}>
          <Header />
          <div className="container">
            <Switch>
              <Route exact path="/" component={Home} />
              <Route exact path="/login" component={Login} />
              <Route exact path="/register" component={Register} />
              <ProtectedRoute path="/hidden" component={HiddenPage} />
            </Switch>
          </div>
        </UserContext.Provider>
      </BrowserRouter>
    </>
  );
}

ProtectedRoute.js

import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
import UserContext from '../../context/UserContext';

export default function ProtectedRoute(props) {
  const { userData } = useContext(UserContext);
  const Component = props.component;

  const isAuthenticated = !!userData.user;
  console.log(isAuthenticated);

  return isAuthenticated ? <Component /> : <Redirect to={{ pathname: '/' }} />;
}
2 Answers

add a loading state...

const [loading, setLoading] = useState(false)

after you check local storage and make axios call, update the state

  if (loading) return null;

  // else render the routes

  return (
     // the regular routes...
  )

You basically want to account for a 3rd state.

Either:

  1. there is a user
  2. there is NO user
  3. the effect has not yet completed

At whatever stage in your useEffect you can confirm that there is no user (unauthenticated) set the user to false.

Since undefined !== false, in your return you can test for both...

if (userData.user === undefined) return 'loading...'

Then you can use your ternary after this line with the knowledge that user has some value. Either false or some user object...

Related