Private (Protected) and Public routes in React Router v6

Muhammad Umais Hassan
9 min readMay 28, 2023

--

I have created an article on protected routes for previous versions of react router (React Router v5) and glad that it helped a lot of people. So, I thought let’s just explore what’s new in the react router v6. You can definitely go and check the official document as well (React Router v6). If you have already gone through it then let’s jump into the main topic.

Setup React App

Let’s begin by creating your react app and installing react router using below commands,

npx create-react-app appname
npm install react-router-dom

Now let’s setup the folder structure and before setup I must say there is free hand for the folder structure it’s just a structure that you can create to follow the article other than that if you have your own project setup go with that just skip this part.
1. Create a folder with the name “pages” inside “src” directory and inside the pages you can create all your pages where you will combine your components. I am going to create 3 files in it, “signup.js”, “signin.js”, “home.js” and there are no components because I am not going deep dive into creating the components and pages.

2. Create a folder with the name “routes” inside “src” directory and inside this folder create three files with the name, “protected.js”, “index.js” and “helpers.js”.

So, after doing this you will have something like,

Folder structure

Coding part

So, let’s jump into the coding part start from pages folder. We will keep our pages very simple,

Our all three pages will look something below,

const Home = () => (
<div>
<h1>Home Screen</h1>
</div>
);

export default Home;
const Signin = () => (
<div>
<h1>Sign in Screen</h1>
</div>
);

export default Signin;
const Signup = () => (
<div>
<h1>Sign up Screen</h1>
</div>
);

export default Signup;

app.js

import Routes from "./routes";

export default function App() {
return <Routes />;
}

So, above three codes are the three pages which are pretty simple we are just displaying simple heading in all the three screens. Also, about the app.js file it is also just a simple file where we will call all of our routes, although at this time we don’t have routes but we are just going to create that.

Now, inside “routes/index.js” file we will create our routes. Let’s see how,

import { RouterProvider } from "react-router-dom";

const Index = () => {
return <RouterProvider router={ROUTER_WE_WILL_CREATE} />;
};

export default Index;

Here we have “RouterProvider” is a newly introduced component in react router v6 which takes all the router data and render.

Let’s now create the routes,

import {
Route,
createBrowserRouter,
createRoutesFromElements
} from "react-router-dom";
import Home from "../pages/home";
import Signin from "../pages/signin";
import Signup from "../pages/signup";

const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/">
<Route index element={<Home />} />
<Route path="signin" element={<Signin />} />
<Route path="signup" element={<Signup />} />
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
)
);

To create our routes we will have few new things in react router v6, first one is “createBrowserRouter”, if you have worked with previous versions of react router you might have used “<BrowserRouter>” it is just an alternative of that which rather than behaving like a component and taking all the routes in its Children actually takes as parameters and inside the paramters of “createBrowserRouter” we have “createRoutesFromElements” which takes all the routes in parameters.

So, let’s just for the reminder I am going to show the old version of creating the same thing, as well as with the new version,

OLD way of creating routes,

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

<BrowserRouter>
<Switch>
<Route path="/" component={Home} exact={true} />
<Route path="/signin" component={Signin} exact={true} />
<Route path="/signup" component={Signup} exact={true} />
</Switch>
</BrowserRouter>

NEW way of creating routes,

import {
Route,
createBrowserRouter,
createRoutesFromElements
} from "react-router-dom";
import Home from "../pages/home";
import Signin from "../pages/signin";
import Signup from "../pages/signup";

const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/">
<Route index element={<Home />} />
<Route path="signin" element={<Signin />} />
<Route path="signup" element={<Signup />} />
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
)
);

So, you can better compare them to see the difference.

Now, we can pass our created router constant to the RouterProvider and the combined code will look something like,

routes/protected.js

import {
Route,
createBrowserRouter,
createRoutesFromElements,
RouterProvider
} from "react-router-dom";
import Home from "../pages/home";
import Signin from "../pages/signin";
import Signup from "../pages/signup";

const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/">
<Route index element={<Home />} />
<Route path="signin" element={<Signin />} />
<Route path="signup" element={<Signup />} />
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
)
);

const Index = () => {
return <RouterProvider router={router} />;
};

export default Index;

We have setup our pages, routes and everything successfully now let’s jump into the protected routes parts.

Protected Routes

To create protected routes we have created a file inside routes file named “protected.js” we are going to write down some code in that file first,

routes/protected.js

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

const Protected = () => {
const token = localStorage.getItem("token");

return token ? <Outlet /> : <Navigate to="/signin" />;
};

export default Protected;

So, here we have created a component that first of all finds token from the local storage, so after logging into the application we normally store our authenticated token into the local storage that we have received from backend, so that we can use it whenever we want or else we store it in our redux it’s just the case how you are handling the authentication part. But, for now you can understand that the “token is just a variable which tracks whether a user is logged in or not” if user will be logged in token will have some value in it and if user will not be logged in it will have null inside it.
Next we are returning something on the bases of token so if there is some value in the token means user is logged in so we can redirect user to the screen where he wants to. And we are using “<Outlet />” here which is also newly introduced thing in react router v6, “<Outlet /> is just an alternative to children previously we passed children component in props but now we can just use Outlet for that”.

Now let’s see how we will use it inside our routes to protect them from unauthorized users. To achieve that,

import Protected from "./protected";

<Route element={<Protected />}>
<Route path="/" element={<Home />} />
// All other routes that you want to protect will go inside here
</Route>

Let’s now integrate it in our app that we have created at first,

routes/index.js

import {
Route,
createBrowserRouter,
createRoutesFromElements,
RouterProvider
} from "react-router-dom";
import Protected from "./protected";
import Home from "../pages/home";
import Signin from "../pages/signin";
import Signup from "../pages/signup";

const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/">
<Route element={<Protected />}>
<Route index element={<Home />} />
{/* All other routes that you want to protect will go inside here */}
</Route>
<Route path="signin" element={<Signin />} />
<Route path="signup" element={<Signup />} />
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
)
);

const Index = () => {
return <RouterProvider router={router} />;
};

export default Index;

So, I have just made home route as protected routes for now but you can place all you routes inside the protected route.

Public Routes

Public routes works on same principle as the protected routes work but for the screens that are accessible by unauthorized users. For, example signin or signup screens are the screen that are accessible by unauthorized users. So, if a user tries to access home screen but he is not authorized then protected routes will redirect user to sign in screen as we have done just in above case. But if the user is logged in and he triess to access sign in screen then he has to navigate to the home screen because he is already logged in so there is no meaning to go to the log in or register screen. So, for these kind of scenarios we use public routes.

Now, to achieve the public routes cases we can actually do it exactly we have done with protected routes we can create a file name public.js and write logic same like we have written for protected if the user is logged in redirect to home screen other wise allow him to access public routes. But there is much simpler way to achieve that, let see how,

Remember we have created a file named “helpers.js” inside routes folder, that file for some purpose and we will use that right now. Let’s go inside that file and write a function,

routes/helpers.js

import { redirect } from "react-router-dom";

export const isAuthenticated = async () => {
const token = localStorage.getItem("token");
if (token) throw redirect("/");
return null;
};

We have just created a function to check if the user is authenticated or not, and on the basis of that we are redirecting user to the public route or some private route.

Now use this function inside our public route,

<Route
path="signin"
element={<Signin />}
loader={async () => await isAuthenticated()}
/>

loader function is a new function that works amazingly but I want to show what loader function can do in upcoming articles because it’s a separate topic. So, stay tuned for that I will attach the link to that article if possible right here after completing that but for now you can just imagine isAuthenticated will either return null or will throw an error, so if it returns null route will work normally and it will allow user to visit the route but if it throw an error then it will do what we have done inside the error handle case. We have actually redirected to home screen.

Let’s now embed this into our app that we are taking along for this article,

import {
Route,
createBrowserRouter,
createRoutesFromElements,
RouterProvider
} from "react-router-dom";
import Protected from "./protected";
import { isAuthenticated } from "./helpers";
import Home from "../pages/home";
import Signin from "../pages/signin";
import Signup from "../pages/signup";

const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/">
<Route element={<Protected />}>
<Route index element={<Home />} />
{/* All other routes that you want to protect will go inside here */}
</Route>
<Route
path="signin"
element={<Signin />}
loader={async () => await isAuthenticated()}
/>
<Route
path="signup"
element={<Signup />}
loader={async () => await isAuthenticated()}
/>
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
)
);

const Index = () => {
return <RouterProvider router={router} />;
};

export default Index;

Why we haven’t used loader for protected routes?

There comes a question in mind that we can do the protected routes stuff with the same logic that we have done for public routes, so why we created protected file for that.

So, the answer to this question is yes you can do that using the same we have done with public routes and it will be something like,

export const handleProtected = async () => {
const token = localStorage.getItem("token");
if (token) throw redirect("/signin");
return null;
};

And we can pass it to the route,

<Route 
path="/"
element={<Home />}
loader={async () => await handleProtected()}
/>

It will work fine but I prefer using the above method of creating protected.js file because for private routes we have normally a large number of files so we will need to pass loader to every route while using the protected component it’s way easier.
Also, another important reason is loader has a lot features we can use it for api calls that I will hopefully show you guys in upcoming articles, and it will make more sense to your guys at that time. But, we can use loader for api calls and some other things so if we will use this function that actually makes some mess, so I prefer the above way of using protected.js file.
Third reason is we have to deal with authorizations as well in this article we have deal with authentications but let suppose we have a website we have role based system I will write down an article on that as well hopefully, but in authorization we have to add some more validations instead of just checking authenticated or not so that also best suits with creating the protected.js component and deal all the things right there.
At the end, I must say you can achieve it both the ways but I prefer using this way so it’s just what you are looking for.

Thanks for reading this article till here, I hope this will be beneficial for you. Let me know if you have any confusions, will happy to assist you.

Happy Learning 😊

--

--