In this article, we are going to use react router version 6 and we are going to create a simple react application with some essential/important elements of a web application, such as protecting routes and having unauthorized and not found pages.
Introduction
In version 6 of react router there were several things that were added and others that were changed, but they brought more flexibility when routing in a web application.
Prerequisites
Before going further, you need:
- NPM
- React
- React Context
- React Router
In addition, it is expected to have a basic knowledge of these technologies.
Getting Started
Create project setup
As a first step, let's scaffold a react app using Vite:
# npm 6.x
npm create vite@latest router-app --template react
# npm 7+, extra double-dash is needed:
npm create vite@latest router-app -- --template react
Then, inside our project folder, we install the following dependency:
npm install react-router-dom --save
Now in our index.html
add the following link for us to use this css framework so we don't deal with classNames:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css"
/>
<title>Vite App</title>
</head>
<!-- ... -->
</html>
With our project configured and the necessary dependencies installed, we can proceed to the next step.
Create Generic Components
First let's create the Not Found page:
// @src/pages/NotFound.jsx
const NotFound = () => (
<div>
<h1>Not Found page</h1>
<p>The page you tried to access doesn't exist.</p>
<p>This is a generic route.</p>
</div>
);
export default NotFound;
With our Not Found page created, we can proceed to create the Unauthorized page:
// @src/pages/Unauthorized.jsx
import { Link } from "react-router-dom";
const Unauthorized = () => (
<div>
<h1>Unauthorized page</h1>
<p>You don't have permission to access this page.</p>
<Link to="/login">Go back to login.</Link>
</div>
);
export default Unauthorized;
As you may have noticed, the <Link />
component of react router was used, which allows us to navigate to other pages, which in this case is to the login page.
Then we can work on our Component which we will name Layout, this component will contain two things. Our navigation bar, with the <Link />
components of the respective pages we want to navigate.
As well as the <Outlet />
component that will be responsible for rendering all the child components, which in this case will be our pages. This will allow us to share the layout between a group of pages.
// @src/components/Layout.jsx
import { Link, Outlet } from "react-router-dom";
const Layout = () => (
<div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/lounge">Lounge</Link>
</li>
</ul>
<Outlet />
</div>
);
export default Layout;
With the generic components created, we can move on to the next step.
Create Auth Context
Our auth context will be responsible for storing data about the user's authentication and from that we will determine whether or not the user has access to certain pages.
The first step is to create the context:
// @src/context/Auth.jsx
import { createContext } from "react";
const AuthContext = createContext(null);
// ...
Then we'll create a hook so we can use the context inside the react components:
// @src/context/Auth.jsx
import { createContext, useContext } from "react";
const AuthContext = createContext(null);
export const useAuth = () => useContext(AuthContext);
// ...
Now we can create our authentication provider:
// @src/context/Auth.jsx
import { createContext, useContext, useState } from "react";
const AuthContext = createContext(null);
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
};
// ...
Still in our authentication context file, we can create a component that will be responsible for determining if the user can access specific routes according to their authentication status.
If he is not authenticated and wants to access a protected route, he will be redirected to the Unauthorized page. Otherwise, you can easily access the routes.
// @src/context/Auth.jsx
import { createContext, useContext, useState } from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
const AuthContext = createContext(null);
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
return (
<AuthContext.Provider value={{ user, setUser }}>
{children}
</AuthContext.Provider>
);
};
export const RequireAuth = () => {
const { user } = useAuth();
const location = useLocation();
if (!user) {
return (
<Navigate
to={{ pathname: "/unauthorized", state: { from: location } }}
replace
/>
);
}
return <Outlet />;
};
Now that we have our authentication context finished, we can move on to the next step.
Create App Pages
First of all, we need to create our main page:
// @src/pages/Home.jsx
const Home = () => {
return (
<div>
<h1>Home page</h1>
<p>This route has public access.</p>
</div>
);
};
export default Home;
Then we can create our login page, where the user needs to enter a username so that he can be logged into our application. Once the submission is made, the user will be redirected to a protected route.
// @src/pages/Login.jsx
import { useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/Auth";
const Login = () => {
const [username, setUsername] = useState("");
const { setUser } = useAuth();
const navigate = useNavigate();
const login = useCallback(
(e) => {
e.preventDefault();
setUser({ username });
navigate("/lounge");
},
[setUser, username]
);
return (
<div>
<h1>Login page</h1>
<p>This route has public access.</p>
<form onSubmit={login}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Type username..."
/>
<button type="submit">Login</button>
</form>
</div>
);
};
export default Login;
With the Login page done, we need to create our protected route. And still on this page we are going to create a function so that the user has the option to log out.
// @src/pages/Lounge.jsx
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/Auth";
const Lounge = () => {
const { user, setUser } = useAuth();
const navigate = useNavigate();
const logout = useCallback(
(e) => {
e.preventDefault();
setUser(null);
navigate("/");
},
[setUser]
);
return (
<div>
<h1>Lounge page</h1>
<p>
Hello <strong>{user?.username}</strong>!
</p>
<p>Looks like you have access to this private route!</p>
<button onClick={logout}>Logout</button>
</div>
);
};
export default Lounge;
With our application pages created, we can move on to the last step.
Define application routes
Before we start, we need to import all the necessary components:
// @src/App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider, RequireAuth } from "./context/Auth";
import Layout from "./components/Layout";
import HomePage from "./pages/Home";
import LoginPage from "./pages/Login";
import NotFoundPage from "./pages/NotFound";
import LoungePage from "./pages/Lounge";
import UnauthorizedPage from "./pages/Unauthorized";
// ...
Next we'll put our AuthProvider as root component and then we'll put the <BrowserRouter />
component and the react router's <Routes />
as child components.
// @src/App.jsx
// Hidden for simplicity
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
{/* ---------- */}
</Routes>
</BrowserRouter>
</AuthProvider>
);
};
export default App;
Next we will define the Layout of our page using our <Layout />
component.
// @src/App.jsx
// Hidden for simplicity
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
{/* ---------- */}
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
);
};
export default App;
Then we can add the pages that can be accessed by the user without being authenticated (including pages related to authorization and not found):
// @src/App.jsx
// Hidden for simplicity
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="*" element={<NotFoundPage />} />
<Route path="/unauthorized" element={<UnauthorizedPage />} />
{/* ---------- */}
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
);
};
export default App;
Last but not least, we can now add our protected pages together with the component responsible for determining if the user can access these routes:
// @src/App.jsx
// Hidden for simplicity
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="*" element={<NotFoundPage />} />
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route element={<RequireAuth />}>
<Route path="/lounge" element={<LoungePage />} />
</Route>
</Route>
</Routes>
</BrowserRouter>
</AuthProvider>
);
};
export default App;
With everything set up, we can now do a little review.
What to expect?
If the user is not logged in, it is expected that he can only access the main and login page. As soon as he tries to access the lounge page, which is protected, he should be redirected to the unauthorized page. Also, if the user tries to access a page that does not exist in the application, the not found page must be rendered.
On the other hand, if the user is logged in, he/she can access all pages of the application, however the user cannot be redirected to the unauthorized page, as he/she is currently logged in to our app.
The result should be similar to the following:
If you want to have access to the source code of this example, you can always click on this link.
Hope you enjoyed this tutorial, stay tuned for more.
Top comments (2)
Thank you!
but what if i dont want the login page to be effected from the layout, how would u solve it?
In that case, you can define the Login route separately, defining the path and the element.