11

How to create a protected route with react-router-dom and storing the response in local storage, so that when a user tries to open next time they can view their details again. After login, they should redirect to the dashboard page.

All functionality is added in ContextApi. Codesandbox link : Code

I tried but was not able to achieve it

Route Page

import React, { useContext } from "react";
import { globalC } from "./context";
import { Route, Switch, BrowserRouter } from "react-router-dom";
import About from "./About";
import Dashboard from "./Dashboard";
import Login from "./Login";
import PageNotFound from "./PageNotFound";

function Routes() {
  const { authLogin } = useContext(globalC);
  console.log("authLogin", authLogin);

  return (
    <BrowserRouter>
      <Switch>
        {authLogin ? (
          <>
            <Route path="/dashboard" component={Dashboard} exact />
            <Route exact path="/About" component={About} />
          </>
        ) : (
          <Route path="/" component={Login} exact />
        )}

        <Route component={PageNotFound} />
      </Switch>
    </BrowserRouter>
  );
}
export default Routes;

Context Page

import React, { Component, createContext } from "react";
import axios from "axios";

export const globalC = createContext();

export class Gprov extends Component {
  state = {
    authLogin: null,
    authLoginerror: null
  };
  componentDidMount() {
    var localData = JSON.parse(localStorage.getItem("loginDetail"));
    if (localData) {
      this.setState({
        authLogin: localData
      });
    }
  }

  loginData = async () => {
    let payload = {
      token: "ctz43XoULrgv_0p1pvq7tA",
      data: {
        name: "nameFirst",
        email: "internetEmail",
        phone: "phoneHome",
        _repeat: 300
      }
    };
    await axios
      .post(`https://app.fakejson.com/q`, payload)
      .then((res) => {
        if (res.status === 200) {
          this.setState({
            authLogin: res.data
          });
          localStorage.setItem("loginDetail", JSON.stringify(res.data));
        }
      })
      .catch((err) =>
        this.setState({
          authLoginerror: err
        })
      );
  };
  render() {
    // console.log(localStorage.getItem("loginDetail"));
    return (
      <globalC.Provider
        value={{
          ...this.state,
          loginData: this.loginData
        }}
      >
        {this.props.children}
      </globalC.Provider>
    );
  }
}

learner62
  • 382
  • 2
  • 7
  • 21

3 Answers3

20

Issue

<BrowserRouter>
  <Switch>
    {authLogin ? (
      <>
        <Route path="/dashboard" component={Dashboard} exact />
        <Route exact path="/About" component={About} />
      </>
    ) : (
      <Route path="/" component={Login} exact />
    )}

    <Route component={PageNotFound} />
  </Switch>
</BrowserRouter>

The Switch doesn't handle rendering anything other than Route and Render components. If you want to "nest" like this then you need to wrap each in generic routes, but that is completely unnecessary.

Your login component also doesn't handle redirecting back to any "home" page or private routes that were originally being accessed.

Solution

react-router-dom v5

Create a PrivateRoute component that consumes your auth context.

const PrivateRoute = (props) => {
  const location = useLocation();
  const { authLogin } = useContext(globalC);
  console.log("authLogin", authLogin);

  return authLogin ? (
    <Route {...props} />
  ) : (
    <Redirect
      to={{
        pathname: "/login",
        state: { from: location }
      }}
    />
  );
};

Update your Login component to handle redirecting back to the original route being accessed.

export default function Login() {
  const location = useLocation();
  const history = useHistory();
  const { authLogin, loginData } = useContext(globalC);

  if (authLogin) {
    const { from } = location.state || { from: { pathname: "/" } };
    history.replace(from);
  }

  return (
    <div
      style={{ height: "100vh" }}
      className="d-flex justify-content-center align-items-center"
    >
      <button type="button" onClick={loginData} className="btn btn-primary">
        Login
      </button>
    </div>
  );
}

Render all your routes in a "flat list"

function Routes() {
  return (
    <BrowserRouter>
      <Switch>
        <PrivateRoute path="/dashboard" component={Dashboard} />
        <PrivateRoute path="/About" component={About} />
        <Route path="/login" component={Login} />
        <Route component={PageNotFound} />
      </Switch>
    </BrowserRouter>
  );
}

Edit how-to-create-a-protected-route-in-react-using-react-router-dom

react-router-dom v6

In version 6 custom route components have fallen out of favor, the preferred method is to use an auth wrapper component.

import { Navigate, Outlet } from 'react-router-dom';

const PrivateRoutes = () => {
  const location = useLocation();
  const { authLogin } = useContext(globalC);
  console.log("authLogin", authLogin);

  return authLogin 
    ? <Outlet />
    : <Navigate to="/login" replace state={{ from: location }} />;
}

...

<BrowserRouter>
  <Routes>
    <Route path="/" element={<PrivateRoutes />} >
      <Route path="/dashboard" element={<Dashboard />} />
      <Route path="/about" element={<About />} />
    </Route>
    <Route path="/login" element={<Login />} />
    <Route path="*" element={<PageNotFound />} />
  </Routes>
</BrowserRouter>

...

export default function Login() {
  const location = useLocation();
  const navigate = useNavigate();
  const { authLogin, loginData } = useContext(globalC);

  if (authLogin) {
    const { from } = location.state || { from: { pathname: "/" } };
    navigate(from, { replace: true });
  }

  return (
    <div
      style={{ height: "100vh" }}
      className="d-flex justify-content-center align-items-center"
    >
      <button type="button" onClick={loginData} className="btn btn-primary">
        Login
      </button>
    </div>
  );
}
Drew Reese
  • 103,803
  • 12
  • 69
  • 96
  • Hi, sir. I have tried your code it was working fine. but I am facing some issues. For me there is no home page, Login Page is the default page once the user tries to log in based on the response it will navigate to the dashboard and the sidebar is available in protected route only. Codesandbox link: https://codesandbox.io/s/protuctedroute-d26tz @Drew – learner62 Feb 22 '21 at 13:01
  • @learner62 The "home" route in my sandbox was only there to provide a starting point that *wasn't* trying to start an authentication flow. I would caution against making your "/" path your authentication path though. That being said your sandbox appears to be working, you just need to ensure you don't create a render loop between your login path "/" and the `PrivateRoute` redirecting back to "/". – Drew Reese Feb 22 '21 at 16:25
  • if you are logged in and refresh the page at a protected route, it will take you away form the page using the code above. this is due to the fact that during a refresh the context is null initially before being hydrated form the database. Any suggestion to how this can be corrected? – Ufenei augustine Jul 29 '21 at 23:49
  • 1
    @Ufeneiaugustine Add a third loading or pending "state" that is neither "authenticated" nor "unauthenticated" whereby you can render null, or a loading spinner, or any other "processing" UI until it is determined whether the user should be allowed on through to the page or if they should be bounced out. – Drew Reese Jul 30 '21 at 23:22
2

For v6:

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

function App() {
  return (
    <Routes>
      <Route path="/public" element={<PublicPage />} />
      <Route
        path="/protected"
        element={
          <RequireAuth redirectTo="/login">
            <ProtectedPage />
          </RequireAuth>
        }
      />
    </Routes>
  );
}

function RequireAuth({ children, redirectTo }) {
  let isAuthenticated = getAuth();
  return isAuthenticated ? children : <Navigate to={redirectTo} />;
}

Link to docs: https://gist.github.com/mjackson/d54b40a094277b7afdd6b81f51a0393f

Eugene
  • 51
  • 3
0

import { v4 as uuidv4 } from "uuid";

const routes = [
    {
        id: uuidv4(),
        isProtected: false,
        exact: true,
        path: "/home",
        component: param => <Overview {...param} />,
    },
    {
        id: uuidv4(),
        isProtected: true,
        exact: true,
        path: "/protected",
        component: param => <Overview {...param} />,
        allowed: [...advanceProducts], // subscription
    },
    {
        // if you conditional based rendering for same path
        id: uuidv4(),
        isProtected: true,
        exact: true,
        path: "/",
        component: null,
        conditionalComponent: true,
        allowed: {
            [subscription1]: param => <Overview {...param} />,
            [subscription2]: param => <Customers {...param} />,
        },
    },
]

// Navigation Component
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Switch, Route, useLocation } from "react-router-dom";

// ...component logic
<Switch>
    {routes.map(params => {
        return (
            <ProtectedRoutes
                exact
                routeParams={params}
                key={params.path}
                path={params.path}
            />
        );
    })}
    <Route
        render={() => {
            props.setHideNav(true);
            setHideHeader(true);
            return <ErrorPage type={404} />;
        }}
    />
</Switch>

// ProtectedRoute component
import React from "react";
import { Route } from "react-router-dom";
import { useSelector } from "react-redux";

const ProtectedRoutes = props => {
    const { routeParams } = props;
    const currentSubscription = 'xyz'; // your current subscription;
    if (routeParams.conditionalComponent) {
        return (
            <Route
                key={routeParams.path}
                path={routeParams.path}
                render={routeParams.allowed[currentSubscription]}
            />
        );
    }
    if (routeParams.isProtected && routeParams.allowed.includes(currentSubscription)) {
        return (
            <Route key={routeParams.path} path={routeParams.path} render={routeParams?.component} />
        );
    }
    if (!routeParams.isProtected) {
        return (
            <Route key={routeParams.path} path={routeParams.path} render={routeParams?.component} />
        );
    }
    return null;
};

export default ProtectedRoutes;

Would like to add highlight never forget to give path as prop to ProtectedRoute, else it will not work.

Nikita Kumari
  • 89
  • 1
  • 4