Protected Routes with React.js and Next.js
Tags: react, nextjs, reactjsdevelopment
Published: 2023-11-17 19:00:03

## Introduction

In order to handle routes in **React.js** we have to install a third party package named **React-Router-Dom**. This package wasn't developed by the React core team but it is a trusted package in React's community.

So this article aims to show ways to implement routes that can only be accessed when the user is somehow authenticated or authorized to have access to specific pages of our applications.

### Creating Protected Routes with React.js

We will start with **React.js** and the first thing we need to do is to install **React-Router-Dom**:

```
npm i react-router-dom
yarn add react-router-dom
pnpm add react-router-dom
```
We will be creating a _router.ts_ file and for that we need to change the structure of our app a bit.

The first step is to create the router file and for that we will create a folder called **router** inside the **src** directory. I am following the naming convention to name all the component files as _index.tsx_.

In the **router** file, first we need to create a functional component named as Router and import 3 components from the package **React-Router-Dom**, **Browser Router**, **Routes** and **Route**.

We will create three pages and so we will have three routes as well.

```
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthGuard } from '../auth/AuthGuard';
import { Signin } from '../view/pages/signin';
import { Signup } from '../view/pages/signup';
import { Dashboard } from '../view/pages/dashboard';

export function Router() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='/signin' element={<Signin />} />
        <Route path='/signup' element={<Signup />} />
        <Route path='/' element={<Dashboard />} />
      </Routes>  
    </BrowserRouter>
  );
}
```
The order is to have the **BrowserRouter** wrapping all the **Routes** and inside the **Routes** we have the specific **Route** file with its path and the element which is the component of the page, they are imported in the first lines of the router component.

The _pages/components_ rendered as elements of each **Route** are created in the **View** folder and inside that we have a folder for each _page/component_.

The second step is to go to the _App.tsx_ file and use our **Router** component there.

```
import { Router } from './router';

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

export default App;
```
And now we will create the **AuthGuard** component that is going to protect the routes and allow us to access the dashboard _page/component_ only after we are signed in. As a good practice we will create a folder called auth and create the **AuthGuard** component there.

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

interface AuthGuardProps {
  isPrivate: boolean;
}

export function AuthGuard({ isPrivate }: AuthGuardProps) {
  const signedIn = false;

  if (!signedIn && isPrivate) {
    return <Navigate to='/signin' replace />;
  }

  if (signedIn && !isPrivate) {
    return <Navigate to='/' replace />;
  }

  return <Outlet />;
}
```

In the **AuthGuard** component we are using two components from **React-Router-Dom**. **Outlet** which is placed in the parent route and it will render the _child routes_, the **Outlet** component will be very useful because we will be using _nested routes_. And the **Navigate** component is the one responsible for redirecting us to the specified route, the property called replace which will not add your current path to the history.

We created a simple interface that contains only one single prop called **isPrivate** and we will use this prop to control if the route requires the user to be authenticated or not.

We have the logic that controls where we will be redirected whether the user is signedIn or the route is private or not and at the end of the component we return the **Outlet** component.

We can also create another **AuthGuard** that requires a token from _localStorage_, in case we wish to implement a different approach.

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

interface AuthGuardProps extends PropsWithChildren;

export function AuthGuard({ children }: AuthGuardProps) {
  if (localStorage.getItem('token')) {
    return children;
  }
  
  return <Navigate to'/' />;
}
```

So now we head back to the Router component and we will use the **AuthGuard** to wrap our **Routes** with it and establish which one is private or not. And the **Route** that will be wrapped by the **AuthGuard** is the one that will be rendered by the **Outlet** component. Since the **AuthGuard** is the parent because it is wrapping the _child route_, thus making _nested routing_.

```
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthGuard } from '../auth/AuthGuard';
import { Signin } from '../view/pages/signin';
import { Signup } from '../view/pages/signup';
import { Dashboard } from '../view/pages/dashboard';

export function Router() {
  return (
    <BrowserRouter>
      <Routes>
        
        {/* Public Routes */}
        <Route element={<AuthGuard isPrivate={false} />}>
          <Route path='/signin' element={<Signin />} />
          <Route path='/signup' element={<Signup />} />
        </Route>
        
        {/* Private Route - Require Authentication */}
        <Route element={<AuthGuard isPrivate />}>
          <Route path='/' element={<Dashboard />} />
        </Route>
        
      </Routes>  
    </BrowserRouter>
  );
}
```

This is the same code as earlier, but now we have our **AuthGuard** component wrapping the **routes**, we are passing the boolean prop isPrivate to indicate which ones are _private_ or _public_ and in the **AuthGuard** component we are returning the **Outlet** component which will render the route that is being wrapped by the **AuthGuard**.

This is a very simple but efficient implementation, easy to read and not that many lines of code.

## Creating Protected Routes with Next.js

For a **Next.js** application we will use a different approach since **Next.js** handles the routes itself with the **Pages Router** system where you can create a file named page and it will become an available route, there's no need for a third-party package like **React-Router-Dom**.

We will start in the main **Layout** file, in there we will wrap the _children_ inside the body with two components: **AuthProvider** and **ProtectRoute**, the **AuthProvider** is a context created in the contexts folder, the code is as follows:

```
'use client';

import { 
  useState, 
  useEffect,
  useContext,
  createContext,     
  PropsWithChildren
} from "react";
import useLocalStorage from "../hooks/useLocalStorage";

interface AuthProviderProps extends PropsWithChildren{};

interface SessionProps {
  access_token?: string;
};

interface ContextProps {
  session: SessionProps;
  isSignedIn: boolean;
  isLoading: boolean;
};

const AuthContext = createContext<ContextProps>({
  session: {},
  isSignedIn: false,
  isLoading: false
});

function AuthProvider({ children }: AuthProviderProps) {
  const [session, setSession] = useState<SessionProps>({});
  const [isLoading, setIsLoading] = useState<boolean>(true);
  const isSignedIn = !!session && !!session?.access_token;
  const storage = useLocalStorage();

  useEffect(() => {
    try {
      const sessionStorage = storage.getItem('session') as SessionProps
      setIsLoading(true);
      
      if (sessionStorage.access_token) {
        setSession(sessionStorage);
        return;
      }
      setSession({});
    } catch (error) {
      setSession({});
      setIsLoading(false);
    } finally {
      setIsLoading(false);
    }
  }, [storage]);

  return (
    <AuthContext.Provider value={{ session, isSignedIn, isLoading }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

export default AuthProvider;
```

In the context we make use of a custom hook called **useLocalStorage( )** to separate the logic for basic operations using the _localStorage_ property, **getItem**, **setItem**, **clear**.

And we are using a third-party package called **React-Secure-Storage** to encrypt and decrypt the data stored in _localStorage_.

First, let's download the package and then we start building the custom hook.

```
npm i react-secure-storage
yarn add react-secure-storage
pnpm add react-secure-storage
```

```
'use client';

import secureLocalStorage from 'react-secure-storage';

function useLocalStorage() {
	const storage = secureLocalStorage;

	function setItem<T>(key: string, value: T){
		storage.setItem(key, value as number | string | object | boolean);
	}

	function getItem(key: string): string |number | object| boolean | null{
		return storage.getItem(key);
	}

	function clear(){
		storage.clear();
	}
  
	return {
		getItem,
		setItem,
		clear
	};
}

export default useLocalStorage;
```
And now we will create the component **ProtectRoute**. This component will have  a function that will determine if the routes are public by using the values provided in the variable publicPages and the hooks from _next/navigation_ **usePathname** and **useRouter**. We are also using **react-hot-toast** which is another third-party package to display toast success, error, warning toast messages throughout our application.

To install it we use the following command:

```
npm i react-hot-toast
yarn add react-hot-toast
pnpm add react-hot-toast
```

```
​​'use client';

import { useAuth } from '@/app/contexts/auth';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { toast } from 'react-hot-toast';

interface ProtectRouteProps {
  children: React.ReactNode;
}

const publicPages = ['/', '/signup'];

function ProtectRoute({ children }: ProtectRouteProps) {
	const { isSignedIn, isLoading } = useAuth();
	const route = useRouter();
	const pathname = usePathname();

	useEffect(() => {
		if (isSignedIn || isLoading || publicPages.includes(pathname)) return;

		route.replace('/');
		toast.error('User not authorized!');
	}, [isSignedIn, isLoading, pathname, route]);

	if(isLoading){
		return (
			<div>
				<center>
					<span>Loading...</span>
				</center>
			</div>
		);
	}

	if (isSignedIn || publicPages.includes(pathname)) {
		return children;
	}
	return null;
}

export default ProtectRoute;
```

Well, I showed two ways of implementing the protection of routes, there are many more different ways, some simpler, some more verbose or performant, but the point here is to say that sooner or later you will end up needing to protect routes in your application due to the authentication flows that most application needs, the goal of this article is to share these two examples, so that you can make some tweakings to adapt and make it suitable for whatever need you may have.

## References

https://reactrouter.com/en/main

https://react.dev/reference/react

https://nextjs.org/docs/pages/building-your-application/routing

https://www.w3schools.com/react/react_router.asp