Day 6: ReactJS - Advancing more deeper in React Routing

Hello there fellow developers. In last blog we learnt about what exactly is Routing in React. In this blog post, we will be diving more and more deeper in the concept of Routing by learning some of its advanced applications.

The deeper you go, the more you are lost. Thats why you need proper "ROUTING" to get out.

Talkin about advanced concepts, in this blog we will be focusing on 3 points:

  • Nested Routes

  • Programatic Navigation using 'useNavigate'

  • Dynamic Routes with URL Parameters

  • Route Guards

  • 404 Page Not Found Handling


Nested Routes:

React Router allows you to define nested routes, which can be helpful when you're creating complex UIs with parent-child relationships. Nested routes allow you to create child routes that live inside parent routes, forming a hierarchy.

Imagine a scenario where a Home page contains two Navigation Tabs (meaning 2 routes to be made) at the top. The first one being "Home" Tab which should be by default. The second route "Dashboard" has more routes inside it. Enough imagining, lets build!

Lets start by wrapping your entire app in a Router component, which enables React Router to handle navigation.

import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <Router>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/dashboard">Dashboard</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard/*" element={<Dashboard />} />
      </Routes>
    </Router>
  );
}

The Dashboard route has child routes inside it, like "Profile" and "Settings." The /* in path="/dashboard/*" indicates that this route can have nested child routes.

Lets see how our Dashboard paths are like:

function Dashboard() {
  return (
    <>
      <h1>Dashboard</h1>
      <nav>
        <Link to="profile">Profile</Link>
        <Link to="settings">Settings</Link>
      </nav>
      <Routes>
        <Route path="profile" element={<Profile />} />
        <Route path="settings" element={<Settings />} />
      </Routes>
    </>
  );
}

Here we use "Link" which allows us to establish the connection. <Link to="profile">Profile</Link> specifies that if the path direction is detected as "profile", then it should render the Profile component. Inside the Dashboard component, we define two child routes: one for Profile and one for Settings. These are nested inside the main Dashboard route. These components (Profile and Settings) will render when you visit /dashboard/profile or /dashboard/settings.

function Profile() {
  return <h2>User Profile</h2>;
}

function Settings() {
  return <h2>Settings</h2>;
}

Anyone's guess! If the path detects /dashboard/profile, then the Profile component will be rendered. Similarly if the path detects /dashboard/settings, then the Settings component will be rendered.

Here is how our final version of App.js should look like:

// App.js
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <Router>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/dashboard">Dashboard</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard/*" element={<Dashboard />} />
      </Routes>
    </Router>
  );
}

function Home() {
  return <h1>Home Page</h1>;
}

function Dashboard() {
  return (
    <>
      <h1>Dashboard</h1>
      <nav>
        <Link to="profile">Profile</Link>
        <Link to="settings">Settings</Link>
      </nav>
      <Routes>
        <Route path="profile" element={<Profile />} />
        <Route path="settings" element={<Settings />} />
      </Routes>
    </>
  );
}

function Profile() {
  return <h2>User Profile</h2>;
}

function Settings() {
  return <h2>Settings</h2>;
}

export default App;

No CSS Applied. You are expected to bear with us throughout this blog !

Bottom line, Nested Routing allows you to create more dynamic Single Page Applications. You can write as many as components you want and route wherever you want.


Programmatic Navigation using useNavigate:

useNavigate is a React Router hook that allows you to change the current URL programmatically, which is useful when you need to navigate based on user actions like form submission or a button click.

Let us learn this concept with the help of simple example. Imagine a page with 2 links, one with Home and the other of Profile.

import { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Router>
  );
}

Now here is the problem statement. In the Home component, we want to navigate to the Profile page when a button is clicked. For that, we are going to use the useNavigate hook.

Now how we can do this. By doing this. (I mean by writing below code).

function Home() {
  const navigate = useNavigate();  // Step 1: Initialize useNavigate

  const goToProfile = () => {
    // Step 2: Use navigate to programmatically change the route
    navigate('/profile');  
  };

  return (
    <div>
      <h1>Home Page</h1>
      <button onClick={goToProfile}>Go to Profile</button>  
        {/* Step 3: Button to trigger navigation */}
    </div>
  );
}

Now let us understand the key concepts used around this code. The useNavigate gives you access to the navigate function, which you can call to change the current route. We are changing the route programmatically by using navigate('profile'). For this, we are using the button which will call the function gotoProfile() which is in fact responsible for programatical navigation. Once the button is clicked, the app navigates to the /profile route and renders the Profile component.

function Profile() {
  return <h2>User Profile</h2>;
}

So final version of our code should look like this:

import { BrowserRouter as Router, Routes, Route, useNavigate } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Router>
  );
}

function Home() {
  const navigate = useNavigate();

  const goToProfile = () => {
    // Programmatically navigate to /profile
    navigate('/profile');
  };

  return (
    <div>
      <h1>Home Page</h1>
      <button onClick={goToProfile}>Go to Profile</button>
    </div>
  );
}

function Profile() {
  return <h2>User Profile</h2>;
}

export default App;

As you can clearly see, the main use of this useNavigate hook is for smooth redirections; preferably during form submissions.


Dynamic Routes with URL Parameters

React Router allows you to pass parameters directly in the URL, which can be used to dynamically render content based on the URL.

Lets say you are browsing someone’s profile on popular social media platform. There is a certain ID associated with different person and we can see it visibly on the address bar. The domain name at the start remains the same, yet the last part changes when we try to browse a different person.

To achieve this, you start by defining a route with a dynamic URL segment, denoted by :id.

import { BrowserRouter as Router, Routes, Route, useParams } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/user/:id" element={<UserProfile />} />  
      </Routes>
    </Router>
  );
}

Here, :id in /user/:id is a dynamic parameter. It will capture whatever value comes after /user/ in the URL (e.g., /user/5 or /user/abc).

But the main question lies here is, how can we access it?

So, In the component linked to the dynamic route, you can extract the URL parameter using the useParams hook.

function UserProfile() {
  const { id } = useParams();  

  return <h2>User Profile for ID: {id}</h2>;  
}

To achieve our goal, we have used useParams hook. This hook returns an object with the URL parameters. In this case, { id } retrieves the id value from the URL. Later on, the id value from the URL is used in the component, so if the URL is /user/5, it will display "User Profile for ID: 5."

Yeah, and lets not forget the by default component i.e. Home which will render at the very beginning.

function Home() {
  return <h1>Home Page</h1>;
}

So our final piece of code should look like this:

import { BrowserRouter as Router, Routes, Route, useParams } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/user/:id" element={<UserProfile />} />
      </Routes>
    </Router>
  );
}

function Home() {
  return <h1>Home Page</h1>;
}

function UserProfile() {
  const { id } = useParams(); // Get the URL parameter

  return <h2>User Profile for ID: {id}</h2>;
}

export default App;

Route Guards

Route guards are a way to control access to specific routes in your app. A typical use case is to prevent users from accessing certain pages unless they are authenticated.

To implement this concept, we'll need an authentication context to store the user's login state. This way, we can share the state across the app and determine whether a user is logged in.

import React, { createContext, useContext, useState } from 'react';

// Create a context for authentication
const AuthContext = createContext();

// Create a provider for the authentication context
export function AuthProvider({ children }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = () => setIsAuthenticated(true);   // Simple login function
  const logout = () => setIsAuthenticated(false); // Simple logout function

  return (
    <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook to use the AuthContext
export function useAuth() {
  return useContext(AuthContext);
}

The context AuthContext holds Authentication State. Later we used provider named AuthProvider, which wraps the components that need access to the authentication state. We have also made use of Custom Hook useAuth() to access the Authentication context as above.

Now our next task is to create a Private route component. The sole purpose of this component will be only to render the protected route if the user is authenticated. Otherwise, it redirects the user to the login page.

import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthProvider'; // Use the auth context

// Create a PrivateRoute component
function PrivateRoute({ children }) {
  const { isAuthenticated } = useAuth();

  // Redirect to login if the user is not authenticated
  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }

  // If authenticated, render the child component (protected page)
  return children;
}

Summarizing in one sentence, this Private Route component acts as a guard that checks if the user is authenticated. If not, it redirects them to the login page (<Navigate to="/login" />).

Now let us design a few components. In a typical scenario, we should not show internal metrics of any website to the user unless he/she is logged in the system. We will create a Login Component, Followed by the Dashboard Component, which we be guarded by the above Private Route component.

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider, useAuth } from './AuthProvider';
import PrivateRoute from './PrivateRoute';

function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          {/* Step 7: Use PrivateRoute for protected routes */}
          <Route
            path="/dashboard"
            element={
              <PrivateRoute>
                <Dashboard />
              </PrivateRoute>
            }
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Our Private Route wraps around the Dashboard Component, so it will only render if the user is authenticated. If the user isn’t logged in and tries to visit /dashboard, they will be redirected to /login.

import { useAuth } from './AuthProvider';
import { useNavigate } from 'react-router-dom';

function Login() {
  const { login } = useAuth(); // Access login function
  const navigate = useNavigate();

  const handleLogin = () => {
    login(); // Log the user in
    navigate('/dashboard'); // Redirect to protected page
  };

  return (
    <div>
      <h1>Login Page</h1>
      <button onClick={handleLogin}>Log In</button>
    </div>
  );
}

login() function is the one which changes the authentication state. Later we have also used useNavigate hook to navigate user to dashboard automatically once login is handled successfully.

Finally the precious component which we are protecting…

function Dashboard() {
  const { logout } = useAuth(); // Logout function

  return (
    <div>
      <h1>Dashboard - Protected Page</h1>
      <button onClick={logout}>Log Out</button>
    </div>
  );
}

Simply a logout() function, which again changes the Authentication state to ‘not logged in’.

This is a bit bigger example, so final code will be much more bigger in size:

import React, { useState, useContext, createContext } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate, useNavigate } from 'react-router-dom';

// 1. Create the AuthContext
const AuthContext = createContext();

// 2. AuthProvider component to wrap the app and manage authentication
function AuthProvider({ children }) {
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const login = () => setIsAuthenticated(true);  // Login function
  const logout = () => setIsAuthenticated(false);  // Logout function

  return (
    <AuthContext.Provider value={{ isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. Custom hook to access authentication state
function useAuth() {
  return useContext(AuthContext);
}

// 4. PrivateRoute component to guard protected routes
function PrivateRoute({ children }) {
  const { isAuthenticated } = useAuth();

  // If not authenticated, redirect to login
  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }

  // Render the protected component if authenticated
  return children;
}

// 5. Home component (Public route)
function Home() {
  return (
    <div>
      <h1>Home Page</h1>
    </div>
  );
}

// 6. Login component (Public route)
function Login() {
  const { login } = useAuth();
  const navigate = useNavigate();

  const handleLogin = () => {
    login();  // Log in the user
    navigate('/dashboard');  // Redirect to dashboard after login
  };

  return (
    <div>
      <h1>Login Page</h1>
      <button onClick={handleLogin}>Log In</button>
    </div>
  );
}

// 7. Dashboard component (Protected route)
function Dashboard() {
  const { logout } = useAuth();

  return (
    <div>
      <h1>Dashboard - Protected Page</h1>
      <button onClick={logout}>Log Out</button>
    </div>
  );
}

// 8. App component (Combines everything and sets up routes)
function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />  {/* Public Home route */}
          <Route path="/login" element={<Login />} />  {/* Public Login route */}
          <Route
            path="/dashboard"
            element={
              <PrivateRoute>  {/* Protect Dashboard route */}
                <Dashboard />
              </PrivateRoute>
            }
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

export default App;

The AuthProvider component manages the authentication state using the useState hook and provides login and logout functions. The useAuth custom hook allows any component within the app to access the authentication state. The PrivateRoute component guards the /dashboard route by checking if the user is authenticated, and if not, it redirects them to the login page. The Home and Login components are publicly accessible, while the Dashboard is a protected component that can only be accessed once the user is logged in. Finally, the App component sets up the routes, wrapping the entire application within the AuthProvider to globally manage authentication state.


404 Page Not Found Handling

Every Developer must be familiar that we get 404 Page Not Found Error whenever specified page route is not found on the web. How can we handle those routes in our code then?

Let us start by creating a simple component for that matter:

function NotFound() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
      <p>Sorry, the page you are looking for does not exist.</p>
    </div>
  );
}

Now, let’s modify the routing logic to render the NotFound component for any unmatched routes. Probably you will recall this concept in Day 5 blog of React Series.

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        {/* Add a route for all unmatched paths */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
}

The path=”*” catches all undefined routes and renders the NotFound component. For example, if the user tries to visit /random-page (which doesn’t exist), the NotFound page will be displayed.

HOMEWORK :- Why don’t you try customizing the 404 page by adding Home button at the top; or more better an image of “ NO ENTRY! “

Finally closing off with full code for handling 404 problem:

import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

// 1. Home component (Public route)
function Home() {
  return (
    <div>
      <h1>Home Page</h1>
      <p>Welcome to the homepage!</p>
    </div>
  );
}

// 2. About component (Public route)
function About() {
  return (
    <div>
      <h1>About Page</h1>
      <p>This is the about page.</p>
    </div>
  );
}

// 3. NotFound component (404 page)
function NotFound() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
      <p>The page you are looking for does not exist.</p>
      <Link to="/">Go back to Home</Link>
    </div>
  );
}

// 4. App component (Sets up routes)
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />  {/* Home route */}
        <Route path="/about" element={<About />} />  {/* About route */}
        <Route path="*" element={<NotFound />} />  {/* Catch-all route for 404 page */}
      </Routes>
    </Router>
  );
}

export default App;

It is very important to handle unspecified routes for UX perspective as this approach ensures that any unrecognized URL will show the custom 404 page instead of a blank or broken page.


So folks lets call it a day.

Well I am too exhausted to write the conclusin paragraph so I will sum up in a single line itself.

These advanced React Router concepts allow you to build dynamic, protected, and user-friendly applications while managing complex routing scenarios.

Make sure you practice well on these concepts and yeah, don’t forget to apply CSS which I was too lazy to apply.

Ciao