'Protected route with react router v6

What is correct way to write a ProtectedRoute with new version 6 of react-router? I wrote this one, but it's not a route

const PrivateRoute = ({ component: Component, ...props }) => {   
  if (!Component) return null;

  return props.isAuthenticated
    ? <Component />
    : <Navigate to={props.redirectLink} /> }

export default PrivateRoute;


Solution 1:[1]

Here is my working example for implementing private routes by using useRoutes.

App.js

import routes from './routes';
import { useRoutes } from 'react-router-dom';

function App() {
  const { isLoggedIn } = useSelector((state) => state.auth);

  const routing = useRoutes(routes(isLoggedIn));

  return (
    <>
      {routing}
    </>
  );
}

routes.js

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

const routes = (isLoggedIn) => [
  {
    path: '/app',
    element: isLoggedIn ? <DashboardLayout /> : <Navigate to="/login" />,
    children: [
      { path: '/dashboard', element: <Dashboard /> },
      { path: '/account', element: <Account /> },
      { path: '/', element: <Navigate to="/app/dashboard" /> },
      {
        path: 'member',
        element: <Outlet />,
        children: [
          { path: '/', element: <MemberGrid /> },
          { path: '/add', element: <AddMember /> },
        ],
      },
    ],
  },
  {
    path: '/',
    element: !isLoggedIn ? <MainLayout /> : <Navigate to="/app/dashboard" />,
    children: [
      { path: 'login', element: <Login /> },
      { path: '/', element: <Navigate to="/login" /> },
    ],
  },
];

export default routes;

Solution 2:[2]

I took this example from react-router-dom: https://github.com/remix-run/react-router/blob/main/examples/auth/README.md

Then modify into this https://stackblitz.com/edit/github-5kknft?file=src%2FApp.tsx

export default function App() {
  return (
    <AuthProvider>
      <Routes>
        <Route element={<Layout />}>
          <Route path="/" element={<PublicPage />} />
          <Route path="/public" element={<PublicPage />} />
          <Route path="/login" element={<LoginPage />} />
          <Route element={<RequireAuth />}>
            <Route path="/protected" element={<ProtectedPage />} />
            <Route path="/dashboard" element={<Dashboard />} />
          </Route>
        </Route>
        <Route path="*" element={<NotFound />} />
      </Routes>
    </AuthProvider>
  );
}
function RequireAuth() {
  let auth = useAuth();
  let location = useLocation();

  if (!auth.user) {
    // Redirect them to the /login page, but save the current location they were
    // trying to go to when they were redirected. This allows us to send them
    // along to that page after they login, which is a nicer user experience
    // than dropping them off on the home page.
    return <Navigate to="/login" state={{ from: location }} />;
  }

  return <Outlet />;
}

Solution 3:[3]

Here is an official guideline from React Router documentation.

Instead of creating wrappers for your <Route> elements to get the functionality you need, you should do all your own composition in the <Route element> prop.

Taking the example from above, if you wanted to protect certain routes from non-authenticated users in React Router v6, you could do something like this:

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

function App() {
  return (
    <Routes>
      <Route path="/public" element={<PublicPage />} />
      <Route
        path="/protected"
        element={
          // Good! Do your composition here instead of wrapping <Route>.
          // This is really just inverting the wrapping, but it's a lot
          // more clear which components expect which props.
          <RequireAuth redirectTo="/login">
            <ProtectedPage />
          </RequireAuth>
        }
      />
    </Routes>
  );
}

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

Notice how in this example the RequireAuth component doesn't expect any of <Route>'s props. This is because it isn't trying to act like a <Route>. Instead, it's just being rendered inside a <Route>.

Solution 4:[4]

Here's my latest working implementation with react-router v6 beta. I don't know how to implement a protected routes with useRoutes though. Their documentation should add an example on how to implement protected/private routes in both ways.

ProtectedRoute component

import React from 'react';
import PropTypes from 'prop-types';
import { Route } from 'react-router-dom';
import Forbidden from '../../views/errors/Forbidden';
import { useAuth } from '../../contexts/AuthContext';

const ProtectedRoute = ({ roles, element, children, ...rest }) => {
  const { user, login } = useAuth();

  if (!user) {
    login();
    return <></>;
  }

  if (roles.length > 0) {
    const routeRoles = roles.map((role) => role.toLowerCase());
    const userRoles = (user && user.roles ? user.roles : []).map((role) => role.toLowerCase());
    if (miscUtils.intersection(routeRoles, userRoles).length === 0) {
      return <Forbidden />;
    }
  }

  return (
    <Route element={element} {...rest}>
      {children}
    </Route>
  );
};

ProtectedRoute.propTypes = {
  roles: PropTypes.arrayOf(PropTypes.string),
  element: PropTypes.element,
  children: PropTypes.node,
};

ProtectedRoute.defaultProps = {
  roles: [],
  element: null,
  children: null,
};

export default ProtectedRoute;

AppRoutes component

import React from 'react';
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
import Login from './components/oauth/Login';
import Logout from './components/oauth/Logout';
import RenewToken from './components/oauth/RenewToken';
import ProtectedRoute from './components/ProtectedRoute';
import NotFound from './views/errors/NotFound';
import Index from './views/Index';
import MainContainer from './views/MainContainer';
import ViewUserProfile from './views/user/profile/ViewUserProfile';
import CreateUserProfile from './views/user/profile/CreateUserProfile';
import UpdateUserProfile from './views/user/profile/UpdateUserProfile';
import PartnerProfile from './views/partner/profile/PartnerProfile';

const AppRoutes = () => {
  return (
    <Routes>
      {/* auth pages (important: do not place under /auth path) */}
      <Route path="oauth/login" element={<Login />} />
      <Route path="oauth/logout" element={<Logout />} />
      <Route path="oauth/renew" element={<RenewToken />} />
      <Route element={<MainContainer />}>
        <Route path="/" element={<Index />} />

        {/* protected routes */}
        <ProtectedRoute path="user" element={<Outlet />}>
          <Route path="/" element={<Navigate to="profile" replace />} />

          <Route path="profile" element={<Outlet />}>
            <Route path="/" element={<ViewUserProfile />} />
            <Route path="create" element={<CreateUserProfile />} />
            <Route path="update" element={<UpdateUserProfile />} />
          </Route>
        </ProtectedRoute>

        <ProtectedRoute path="partner" roles={['partner']} element={<Outlet />}>
          <Route path="/" element={<Navigate to="profile" replace />} />
          <Route path="profile" element={<PartnerProfile />} />
        </ProtectedRoute>
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
};

export default AppRoutes;

Solution 5:[5]

You would need to write a small wrapper and use Navigate component to redirect. Also you need to render a route

const Container = ({Component, redirectLink, isAuthenticated, ...props}) => {
  if(!isAuthenticated) {
       return <Navigate to={redirectLink} />;
   }
   
   return <Component {...props} />
}
const PrivateRoute = ({ component: Component, redirectLink, isAuthenticated, path, ...props }) => {   

  return (
    <Route
        path={path}
        element={<Container redirectLink={redirectLink} isAuthenticate={isAuthenticated} Component={Component} />}
    />
)

export default PrivateRoute;

You can find the migration guidelines here on the github docs

Solution 6:[6]

All good options. You can also simply render different route handling based on auth state (or any other state). You don't have to use the raw Javascript object method.

Don't forget you can use an immediately invoked anonymous inner function (() => COMPONENT)() to dynamically decide what component handles a particular <Route/>.

The examples may not yet be in the preliminary documentation for v6 because handling private <Route/>s is actually surprisingly simple.

E.g.

<Routes>
      {state.authed ?
        // Wait until we have the current user...
        currentUser ?
          <Route
            path='/'
            element={(() => {
              // Show a "no access" message if the user is NOT an App Admin doesn't have access to any schools at all (which includes not having access to anything INSIDE any school either)
              if (!currentUser.appAdministrator && currentUser.schoolIds?.length === 0) return <AdminNoAccess />
              return <Outlet />
            })()}
          >
            <Route
              path='/'
              element={(() => {
                // If the user is a super user, we return the <SuperAdmin /> component, which renders some of its own routes/nav.
                if (currentUser.appAdministrator) return <SuperAdmin />
                return <Outlet />
              })()}
            >
              <Route
                path='schools'
                element={(() => {
                  if (currentUser.schoolIds?.length === 1) {
                    return <Navigate to={`schools/schoolId`} />
                  } else {
                    return <AdminSchools />
                  }
                })()}
              />

              <Route path='users' children={<Users />} />
            </Route>

            <Route path={`schools/:schoolId`} element={<AdminSchool />} />

            <Route path='*' element={<Navigate to='schools' />} />
          </Route>
          :
          null
        :
        <>
          <Route path='login' element={<Login />} />
          <Route path='signup' element={<Signup />} />
          <Route path='forgot-password' element={<ForgotPassword />} />
          <Route path='reset-password' element={<ResetPassword />} />

          <Route path='*' element={<Navigate to='login' />} />
        </>
      }
    </Routes>

Solution 7:[7]

Here is a working example.

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

const PrivateRoute = ({ component: Component, redirectTo, isAuth, path, ...props }) => {
    if(!isAuth) {
        return <Navigate to={redirectTo} />;
    }
    return <Route path={path} element={<Component />} />
};

export default PrivateRoute;

Usage:

<Routes>
     <Route path="app" element={<DashboardLayout />}>
         <PrivateRoute isAuth={true} path="account" component={AccountView}  redirectTo='/login'/>
     </Route>
 </Routes>

Solution 8:[8]

Here's a slightly more TypeScript friendly implementation that reuses RouteProps from react-router v6:

import React from 'react';
import { RouteProps } from 'react-router';
import { Route, Navigate } from 'react-router-dom';
import { useAuthState } from '../../contexts';

export interface PrivateRouteProps extends RouteProps {
  redirectPath: string;
}

export const PrivateRoute = ({ redirectPath, ...props }: PrivateRouteProps) => {
  const { user } = useAuthState();
  if (!user) {
    return <Navigate to={redirectPath} />;
  }
  return <Route {...props} />;
};

useAuthState is a hook that is able to retrieve the user if one is logged in.

This is how I use it:

<Routes>
  <Route path="/" element={<Home />} />
  <PrivateRoute path="/admin" redirectPath="/signin" element={<Admin />} />
  <Route path="*" element={<NotFound />} />
</Routes>

Solution 9:[9]

This is the structure for the BrowserRouter as Router:

const AppRouter = () => {
  return (
    <Router>
      <Layout>
        <Routes>
          <Route exact path="" element={<Home />} />
          <Route exact path="login" element={<Login />} />
          <Route exact path="register" element={<Register />} />

          // These are the Private Components
          <Route
            exact
            path="/account"
            element={
              <PrivateRoute>
                <Account />
              </PrivateRoute>
            }
          />

          <Route
            exact
            path="/quizzes"
            element={
              <PrivateRoute>
                <Quizzes />
              </PrivateRoute>
            }
          />

          <Route
            exact
            path="/quizz/:quizzid"
            element={
              <PrivateRoute>
                <Quizz />
              </PrivateRoute>
            }
          />

          <Route
            exact
            path="/admin/users"
            element={
              <PrivateRoute>
                <Users />
              </PrivateRoute>
            }
          />
          <Route exact path="*" element={<NotFound />} />
        </Routes>
      </Layout>
    </Router>
  );
};

This is the PrivateRoute:

import { Navigate } from "react-router-dom";
import { useAuth } from "../auth/useAuth";

function PrivateRoute({ children }) {
  const auth = useAuth();
  return auth.user ? children : <Navigate to="/login" />;
}

export default PrivateRoute;

Solution 10:[10]

By using replace attribute we prevent the user to use the Back browser button.

PrivateRoute.js

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

const PrivateRoute = ({ currentUser, children, redirectTo }) => {
  if (!currentUser) return <Navigate to={redirectTo} replace />;

  return children;
};

export default PrivateRoute;

Implementation:

<Routes>
   <Route path='signIn' element={
            <PrivateRoute currentUser={currentUser} redirectTo='/'>
              <SignInAndSignUpPage />
            </PrivateRoute>
          }
        />
<Routes/>

Solution 11:[11]

I don't know if this is the right way to do it but you don't actually need a private route component. You can just put all the private routes inside a component and render it conditionally as follows. In the below code, I put all the private routes inside the Private component and all the open routes inside the Public component.

function RootRouter() {

  return (
    <div>
      <Router>
        {isLoggedIn ? <PrivateRouter /> : <PublicRouter />}
      </Router>
    </div>
  );
}

function PrivateRouter(props) {
  return (
    <div>
      <ToastContainer autoClose={3000} hideProgressBar />
      <NavBar />
      <Routes>
        <Route path="/" exact element={<Home />} />
        <Route path="/add" element={<Add />} />
        <Route path="/update/:id" element={<Add />} />
        <Route path="/view/:id" element={<Employee />} />
      </Routes>
    </div>
  );
}

function PublicRouter() {
  return (
    <Routes>
      <Route path="/" element={<Login />} />
    </Routes>
  );
}

You can also use switch cases to allow access based on the role of the user.

Note: you dont have a create a seperate components, you can actually put all the routes in a single component and render it using same conditions.

Solution 12:[12]

You could use auth-react-router package https://www.npmjs.com/package/auth-react-router

It provides a really simple API to define your routes and few more configurations (like redirect routes for authorized and unauthorized routes, fallback component for each of the routes)

usage:

  1. define routes
// routes.tsx

import React from 'react';
import { IRoutesConfig } from 'auth-react-router';
import LoginPage from '../pages/LoginPage.tsx';

// public lazy loaded pages
const LazyPublicPage = React.lazy(() => import('../pages/PublicPage.tsx'));

// private lazy loaded pages
const LazyPrivatePage = React.lazy(() => import('../pages/PrivatePage.tsx'));
const LazyProfilePage = React.lazy(() => import('../pages/ProfilePage.tsx'));


export const routes: IRoutesConfig = {
  publicRedirectRoute: '/profile', // redirect to `/profile` when authorized is trying to access public routes
  privateRedirectRoute: '/login', // redirect to `/login` when unauthorized user access a private route
  defaultFallback: <MyCustomSpinner />,
  public: [
    {
      path: '/public',
      component: <LazyPublicPage />,
    },
    {
      path: '/login',
      component: <LoginPage />,
    },
  ],
  private: [
    {
      path: '/private',
      component: <LazyPrivatePage />,
    },
    {
      path: '/profile',
      component: <LazyProfilePage />
    },
  ],
  common: [
    {
      path: '/',
      component: <p>common</p>,
    },
    {
      path: '*',
      component: <p>page not found 404</p>,
    },
  ],
};
  1. link them to your application
import { AppRouter, Routes } from 'auth-react-router';
import { BrowserRouter } from 'react-router-dom';
import { routes } from './routes';

export const App = () => {
  const { isAuth } = useAuthProvider();
  return (
    <BrowserRouter>
      <AppRouter isAuth={isAuth} routes={routes}>
        {/* Wrap `Routes` component into a Layout component or add Header */}
        <Routes />
      </AppRouter>
    </BrowserRouter>
  );
};

Solution 13:[13]

I tried to use all the above stated ways but don't know why nothing worked for me. Finally i solved it and here is my solution to the same:

First make one file of name AdminRoute.js in "routes" folder somewhere in src.

import { Typography } from "@mui/material";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { Navigate } from "react-router-dom";
import { currentAdmin } from "../../functions/auth";
import LoadingToRedirect from "./LoadingToRedirect";

const AdminRoute = ({ children }) => {
  const { user } = useSelector((state) => ({
    ...state,
  }));
  const [ok, setOk] = useState(false);

  useEffect(() => {
    if (user && user.token) {
      currentAdmin(user.token)
        .then((res) => {
          console.log("CURRENT ADMIN RES", res);
          setOk(true);
        })
        .catch((err) => {
          console.log("ADMIN ROUTE ERR", err);
          setOk(false);
        });
    }
  }, [user]);
  return ok ? children : <LoadingToRedirect />;
};

export default AdminRoute;

Here you can have your own logic to decide when will user e redirected and when he will not.Like in my case i'm Checking if role of user is admin or not by making one api call.

Then make one LoadingToRedirect.js file in the same "routes" folder.

import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";

const LoadingToRedirect = () => {
  const [count, setCount] = useState(5);
  let navigate = useNavigate();

  useEffect(() => {
    const interval = setInterval(() => {
      setCount((currentCount) => --currentCount);
    }, 1000);
    // redirect once count is equal to 0
    count === 0 && navigate("/");
    // cleanup
    return () => clearInterval(interval);
  }, [count, navigate]);

  return (
    <div className="container p-5 text-center">
      <p>Redirecting you in {count} seconds</p>
    </div>
  );
};

export default LoadingToRedirect;

Now set up your App.js in your app.js:

Here when you go to '/check' url, the private route functionalities will come in action and it will check if the user is 'admin' or not.Here is the page which is to be protected and it acts as 'children' to

<Routes>
          <Route
            path="/check"
            element={
              <AdminRoute>
                <Check />
              </AdminRoute>
            }
          />

     
          <Route path="*" element={<NotFound />} />
        </Routes>

That's it You are ready to go. Cheers!!

Solution 14:[14]

This has worked for me.

const protectedPages = [
    { path: '/packages', page: <PackagesPage /> },
    { path: '/checkout', page: <CheckoutPage /> },
    { path: '/checkout/success', page: <CheckoutSuccessPage /> },
];

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path='/' element={<AuthPage />} />

                {/* Programmatically protect routes */}
                {protectedPages.map((p, i) => (
                    <Route
                        path={p.path}
                        element={<RequireAuth>{p.page}</RequireAuth>}
                        key={i}
                    />
                ))}

                <Route path='*' element={<PageNotFound />}></Route>
            </Routes>
        </BrowserRouter>
    );
}

const RequireAuth = ({ children }) => {
    const navigate = useNavigate();
    const { id } = useSelector(selectUser);

    React.useEffect(() => {
        if (!id) {
            navigate('/');
        }
    }, [id, navigate]);

    return children;
};

export default App;