18

How to create a protected route with react-router-dom and storing the response in localStorage, 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>
    );
  }
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
learner62
  • 520
  • 3
  • 10
  • 26

7 Answers7

42

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 Redirect 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 v6

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

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

const PrivateRoutes = () => {
  const location = useLocation();
  const { authLogin } = useContext(globalC);

  if (authLogin === undefined) {
    return null; // or loading indicator/spinner/etc
  }

  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>

or

const routes = [
  {
    path: "/",
    element: <PrivateRoutes />,
    children: [
      {
        path: "dashboard",
        element: <Dashboard />,
      },
      {
        path: "about",
        element: <About />
      },
    ],
  },
  {
    path: "/login",
    element: <Login />,
  },
  {
    path: "*",
    element: <PageNotFound />
  },
];

...

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

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

  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>
  );
}

react-router-dom v5

Create a PrivateRoute component that consumes your auth context.

const PrivateRoute = (props) => {
  const location = useLocation();
  const { authLogin } = useContext(globalC);

  if (authLogin === undefined) {
    return null; // or loading indicator/spinner/etc
  }

  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);

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

  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

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 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
  • Drew, Can you show what the `globalC` context would look like in the v6 example? – dmikester1 Aug 26 '22 at 16:35
  • @dmikester1 Do you mean something other than `const globalC = React.createContext({ authLogin: false, loginData: {} });`? Or in the broader sense that includes the entire `Provider` implementation? That would be a bit off-topic for this question, but the gist is that this line of code represents whatever logic you are using to store and provide the auth status and handling. It could be a simple React context, or Redux, or some other global state management. How you access it depends on what your app is using/how it's implemented. – Drew Reese Aug 26 '22 at 18:26
8

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

Yauhen
  • 401
  • 6
  • 7
1

If you want an easy way to implement then use Login in App.js, if user is loggedin then set user variable. If user variable is set then start those route else it will stuck at login page. I implemented this in my project.

return (
<div>
  <Notification notification={notification} type={notificationType} />

  {
    user === null &&
    <LoginForm startLogin={handleLogin} />
  }

  {
    user !== null &&
    <NavBar user={user} setUser={setUser} />
  }

  {
    user !== null &&
    <Router>
      <Routes>
        <Route exact path="/" element={<Home />} />
        <Route exact path="/adduser" element={<AddUser />} /> />
        <Route exact path="/viewuser/:id" element={<ViewUser />} />
      </Routes>
    </Router>
  }

</div>

)

Harsh Tripathi
  • 109
  • 1
  • 5
1

This Answer for Newer of Reactjs

React Router Dom: ^ v6

//App.js
<Route path="/"
       element={<HomePage /> } />

<Route path="/dashboard"
       element={<AuthMiddleware> <DashboardPage /> </AuthMiddleware>}
/>
// AuthMiddelware.js
import { Navigate } from "react-router-dom"

export const AuthMiddleware = (props) => { 
    
    const token = true; // get login status here
    
    const { auth=token, children, redirect = '/login' } = props; 

    if (!auth) {
       return <Navigate to={redirect} replace />;
    }
    return children;
 };
GMKHussain
  • 3,342
  • 1
  • 21
  • 19
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
  • 309
  • 3
  • 7
0

Here is an easy react-router v6 protected route. I have put all the routes I want to protect in a routes.js:-

const routes = [{ path: "/dasboard", name:"Dashboard", element: <Dashboard/> }]

To render the routes just map them as follows: -

<Routes>
   {routes.map((routes, id) => {
     return(
     <Route
         key={id}
         path={route.path}
         exact={route.exact}
         name={route.name}
         element={
            localStorage.getItem("token") ? (
                route.element
             ) : (
                <Navigate to="/login" />
             )
         }
     )
   })
  }
</Routes>
cmoshe
  • 329
  • 4
  • 7
0
import { BrowserRouter as Router } from "react-router-dom";        
import { Routes, Route } from "react-router-dom";      
import Home from "./component/home/Home.js";      
import Products from "./component/Products/Products.js";      
import Profile from "./component/user/Profile.js";      
      
import ProtectedRoute from "./component/Route/ProtectedRoute";      
function App() {      
 return (      
    <Router>      
    <Routes>      
        <Route path="/" element={<Home />} />      
        <Route path="/products" element={<Products />} />      
        <Route      
          path="/account"      
          element={      
            <ProtectedRoute  >      
              <Profile />      
            </ProtectedRoute>      
          }      
        />      
      </Routes>      
       </Router>      
  );      
}      
      
//ProtectedRoute       

export default App;      
import React from "react";      
import { useSelector } from "react-redux";      
import { Navigate } from "react-router-dom";      

function ProtectedRoute({ children }) {      
  const { isAuthecated, loading } = useSelector((state) => state.user);      
      
  if (!isAuthecated) {      
    return <Navigate to="/login" replace />;      
  }      
  return children;      
}      
export default ProtectedRoute;      
      
  • 1
    As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jun 08 '23 at 11:59
  • 1
    You should add explanation. – Super Kai - Kazuya Ito Jun 09 '23 at 00:03