mevrick
mevrick

Reputation: 1045

React-router: ProtectedRoute authentication logic not working

I am trying to implement protected routes that are only accessible after the user logs in.

Basically, there are two routes: /login Login component (public) and / Dashboard component (protected). After the user clicks on the Login button in /login, an API is called which returns an accessToken, which is then stored in localStorage. The protectedRoute HOC checks if the token is present in the localStorage or not and redirects the user accordingly.

After clicking on the Login button it just redirects back to the login page instead of taking user to Dashboard. Not sure what is wrong with the logic.

asyncLocalStorage is just a helper method for promise based localStorage operations.

App.js

import { useEffect, useState } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Dashboard from "./Dashboard";
import Login from "./Login";
import ProtectedRoute from "./ProtectedRoute";
import { asyncLocalStorage } from "./asyncLocalStorage";

function App() {
  const [auth, setAuth] = useState(false);

  useEffect(() => {
    const getToken = async () => {
      const token = await asyncLocalStorage.getItem("accessToken");
      if (token) setAuth(true);
    };
    getToken();
  }, []);

  return (
    <div className="app">
      <Router>
        <Switch>
          <ProtectedRoute exact path="/" isLoggedIn={auth} redirectTo="/login">
            <Dashboard />
          </ProtectedRoute>

          <Route exact path="/login">
            <Login />
          </Route>
        </Switch>
      </Router>
    </div>
  );
}

export default App;

ProtectedRoute.js

import { Route, Redirect } from "react-router-dom";

const ProtectedRoute = ({ children, isLoggedIn, redirectTo, ...rest }) => {
  return (
    <Route
      {...rest}
      render={() => {
        return isLoggedIn ? children : <Redirect to={redirectTo} />;
      }}
    />
  );
};

export default ProtectedRoute;

Dashboard.js

const Dashboard = () => {
  return (
    <div>
      <h3>Dashboard</h3>
      <button
        onClick={() => {
          localStorage.removeItem("accessToken");
          window.location.href = "/login";
        }}
      >
        Logout
      </button>
    </div>
  );
};

export default Dashboard;

Login.js

import { asyncLocalStorage } from "./asyncLocalStorage";

const Login = () => {
  return (
    <div>
      <h3>Login</h3>
      <button
        onClick={async () => {
          // Making an API call here and storing the accessToken in localStorage.
          await asyncLocalStorage.setItem(
            "accessToken",
            "SOME_TOKEN_FROM_API_RES"
          );
          window.location.href = "/";
        }}
      >
        Login
      </button>
    </div>
  );
};

export default Login;

asyncLocalStorage.js

export const asyncLocalStorage = {
  setItem: (key, value) => {
    Promise.resolve(localStorage.setItem(key, value));
  },
  getItem: (key) => {
    return Promise.resolve(localStorage.getItem(key));
  }
};

Upvotes: 0

Views: 527

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42188

Problem: auth State Never Updated

Your ProtectedRoute component relies on the value of auth from the useState in App in order to determine whether the user is logged in or not. You set this value through a useEffect hook which runs once when the App is mounted. This hook is never run again and setAuth is never called anywhere else. If the user is logged out when App first mounts then the ProtectedRoute will always receive isLoggedIn={false} even after the user has logged in and the local storage has been set.

Solution: call setAuth from Login

A typical setup would check localStorage from the ProtectedRoute, but I don't think that will work with the async setup that you have here.

I propose that you pass both isLoggedIn and setAuth to the Login component.

<Login isLoggedIn={!!auth} setAuth={setAuth}/>

In the Login component, we redirect to home whenever we get a true value of isLoggedIn. If a users tries to go to "/login" when they are already logged in, they'll get redirected back. When the log in by clicking the button, they'll get redirected when the value of auth changes to true. So we need to change that value.

The onClick handler for the button will await setting the token in storage and then call setAuth(token). This will trigger the redirection.

import { asyncLocalStorage } from "./asyncLocalStorage";
import {Redirect} from "react-router-dom";
const Login = ({isLoggedIn, setAuth}) => {
  if ( isLoggedIn ) {
    return <Redirect to="/"/>
  }
  return (
    <div>
      <h3>Login</h3>
      <button
        onClick={async () => {
          // Making an API call here and storing the accessToken in localStorage.
          const token = "SOME_TOKEN_FROM_API_RES";
          await asyncLocalStorage.setItem(
            "accessToken",
            token
          );
          setAuth(token);
        }}
      >
        Login
      </button>
    </div>
  );
};

export default Login;

Upvotes: 1

Related Questions