How To Protected Routes In React 6 Using The Context Hook Enter a brief summary for this post

How To Protected Routes in React 6 Using the Context Hook

With front-end experience in HTML, CSS and JQuery I see it essential to have experience too in one of the popular Javascript frameworks. The clear choice was either React or Vue. I opted for React as I liked the JSX syntax. I guess when you start learning the framework the next milestone after mastering nested routes is securing those routes.

The Context Hook

That's where the Context Hook helps you out. You use that to set up your authentication using a provider ― no need to pass props down through the hierarchy, complicating matters. The complete hook is generic in nature and simple to understand. As an application is further developed this global provider daresay will become more complicated.

import { createContext, useState } from "react";

/* create the global state, from context */
const AuthContext = createContext({});

export const AuthProvider = ({children}) => {
	const [auth, setAuth] = useState({});

	return (
		<AuthContext.Provider value={{auth, setAuth}}>{children}</AuthContext.Provider>
	);
};

export default AuthContext;

As well as the Context provider you need to create a custom hook that'll facilitate the use of the provider. This hook is below and with only a few lines of Javascript you can do amazing things.

import { useContext } from "react";
import AuthContext from "../Utils/AuthProvider";

const useAuth = () => {
    return useContext(AuthContext);
}

export default useAuth;

Now to actually secure those private routes in question, you implement this hook (and not the provider). Your initial App.js therefore looks like this below. Let's just say this is the first way of securing your routes then.

 function App() {
    return (
        <BrowserRouter>
            <AuthProvider>
                <Routes>
                    <Route element={<Public />}>
                        <Route index element={<Home />} />
                        <Route path="about" element={<About />} />
                        <Route path="contact" element={<Contact />} />
                        <Route path="blog" element={<Blog />} />
                        <Route path="login" element={<Login />} />

                        <Route element={<Private />}>
                            <Route path="dashboard" element={<Dashboard />}>
                                <Route path="posts" element={<Posts />} />
                                <Route path="topics" element={<Topics />} />
                                <Route path="comments" element={<Comments />} />
                                <Route path="logout" element={<Logout />} />
                            </Route>
                        </Route>
                        <Route path="*" element={<Missing />} />
                    </Route>
                </Routes>
            </AuthProvider>
        </BrowserRouter>
    );
};

export default App;

On the other hand, the structure is slightly different for the second way (below). It's important you can understand the subtle difference and why. Because the first approach requires a further route to ensure the children of the nested (private) routes can be rendered and shown.

const App = () => {
    return (
        <>
            <BrowserRouter>
                <AuthProvider>
                    <Routes>
                        <Route element={<Public />}>
                            <Route index element={<Home />} />
                            <Route path="about" element={<About />} />
                            <Route path="contact" element={<Contact />} />
                            <Route path="login" element={<Login />} />

                            <Route path="dashboard" element={<Private />}>
                                <Route path="posts" element={<Posts />} />
                                <Route path="topics" element={<Topics />} />
                                <Route path="logout" element={<Logout />} />
                            </Route>

                            <Route path="*" element={<Missing />} />
                        </Route>
                    </Routes>
                </AuthProvider>
            </BrowserRouter>
        </>
    );
};

export default App;

The advantage (if there is one) of the first approach is there is no need for a useEffect hook. The <Private /> component for the first solution I use the following.

import { Outlet, Navigate, useLocation } from "react-router-dom";
import useAuth from "../Hooks/useAuth";

function Private() {
    const location = useLocation();
    const {auth, setAuth} = useAuth();

    return (
        auth?.token ? 
            <Outlet /> : 
            <Navigate to="/login" state={{ from: location }} replace />
    );
	
};

export default Private;

If the authenticated user token (a JWT token) isn't found then you are redirected to the login page. Otherwise React's <Outlet /> renders all the necessary children that are nested. Not all of them of course, only the one specified by what's in the address bar. The second approach is this one which does away with the need for an additional route but must use a useEffect hook. Which may not be a problem depending on your point of view.

I don't care personally: if the darned thing works that's great.

import { Outlet, useNavigate } from "react-router-dom";
import useAuth from "../Hooks/useAuth";

const PrivateLayout = () => {
    const navigate = useNavigate();
    const {auth, setAuth} = useAuth();

    useEffect(() => {
        if(!auth?.token) {
            navigate("/login", { replace: true });
        } 
    });

    return (
        <>  
            <h2>Dashboard</h2>
            <br />
            <Link to="/dashboard">Dashboard</Link>
            <Link to="posts">Posts</Link>
            <Link to="topics">Topics</Link>
            <Link to="logout">Logout</Link>
            <Outlet />
        </>
    );
};

export default Private;

The JSX you see here, in the component of the second approach is encapsulated in its own component remember ― <Dashboard /> (see below) for the first approach. Confused? Start from the beginning again and go through it all a second time.

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

function Dashboard() {
	return (
        <>  
            <h2>Dashboard</h2>
            <br />
            <Link className="text-dark px-2" to="/dashboard">Dashboard</Link>
            <Link className="text-dark px-2" to="posts">Posts</Link>
            <Link className="text-dark px-2" to="topics">Topics</Link>
            <Link className="text-dark px-2" to="comments">Comments</Link>
            <Link className="text-dark px-2" to="logout">Logout</Link>
            <Outlet />
        </>
    );
};

export default Dashboard;

Happily, the login mechanism is the same for both approaches. It's where you set up your useAuth hook.

/// the login component...
const navigate = useNavigate();
    const {auth, setAuth} = useAuth();
    
    const loginHandler = () => {
        setAuth({
            token: "--TOKEN--"
        });

        navigate("/dashboard", { replace: true });
    };
// ... etc ...

You set up a form to collect user input and submit it to the server and manage the response in an event handler. Once the user has been found you store their JWT token in the hook and navigate away. But the work of this blog post isn't finished, not quite yet anyway.

Because you must hide and show the login and logout links accordingly. You do that in the <Public /> component, which nests the entire structure (inclusive of the private area).

import { Link, Outlet } from "react-router-dom";
import { AuthProvider } from "../Utils/AuthProvider";
import useAuth from "../Hooks/useAuth";

function Public() {
    const {auth, setAuth} = useAuth();

	return (
        <>  
            <h1>Public Layout</h1>
            <br />
            <Link to="/">Home</Link>
            <Link to="about">About</Link>
            <Link to="contact">Contact</Link>
            <Link to="blog">Blog</Link>
            {!auth?.token && <Link to="login">Login</Link>}
            {auth?.token && <Link to="dashboard">Dashboard</Link>}

            <Outlet />
        </>
    );
};

export default Public;

Now everything falls into place. I am sure there are other ways to secure routes in React because there is so much freedom within the framework but for me these are the two approaches I favour. In any case hopefully you can take something from this if you are learning React? Because either starting out your coding career or coming from a server background with experience of programming languages, Facebook's framework takes some adjustment to it.