learner62
learner62

Reputation: 610

How to create a protected route with react-router-dom?

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

Upvotes: 27

Views: 37002

Answers (7)

Drew Reese
Drew Reese

Reputation: 203466

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 and v7

In version 6/7 custom route components no longer work, the preferred method is to use an auth layout component.

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

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

1 React-Router-DOM v7 react-router is the primary entry point, import components from react-router instead of react-router-dom. See v7 Installation for details.

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

Upvotes: 54

Harsh Tripathi
Harsh Tripathi

Reputation: 201

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

Upvotes: 2

Yauhen
Yauhen

Reputation: 469

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

Upvotes: 10

GMKHussain
GMKHussain

Reputation: 4709

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

Upvotes: 2

ritik Kumar Sharma
ritik Kumar Sharma

Reputation: 27

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;      
      

Upvotes: 1

cmoshe
cmoshe

Reputation: 369

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>

Upvotes: 1

Nikita Kumari
Nikita Kumari

Reputation: 333

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.

Upvotes: 0

Related Questions