DEV Community

Cover image for Protected Routes with React.js and Next.js
Hugo Ramon Pereira
Hugo Ramon Pereira

Posted on

Protected Routes with React.js and Next.js

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 />;
}
Enter fullscreen mode Exit fullscreen mode

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'/' />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
'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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
​​'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;
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)