Configuring Routes and Parameters

Advanced Route Configuration and Parameter Handling in React Router

Advanced Route Configuration

In our previous lecture, we covered the basics of setting up routes with React Router. Now we'll explore more advanced route configuration options and patterns.

Dynamic Route Segments

React Router allows for highly customizable route paths with dynamic segments. Each dynamic segment is prefixed with a colon and becomes a parameter that can be accessed via useParams().


<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/users/:userId/posts/:postId" element={<UserPost />} />
<Route path="/products/:category/:productId" element={<ProductDetail />} />
        

Accessing multiple parameters in a component:


function UserPost() {
  const { userId, postId } = useParams();
  
  return (
    <div>
      <h1>User {userId}'s Post {postId}</h1>
      {/* Content here */}
    </div>
  );
}
        

Optional Parameters

To make a route parameter optional, you can use the ? modifier. This allows the route to match both with and without the parameter.


// This route will match both "/users" and "/users/123"
<Route path="/users/:userId?" element={<Users />} />

function Users() {
  const { userId } = useParams();
  
  if (userId) {
    return <UserProfile userId={userId} />;
  } else {
    return <UsersList />;
  }
}
        

Wildcard Routes and Splats

React Router has special syntax for creating wildcard routes that can match any pattern. The * (splat) parameter captures all remaining portions of the URL.


// Match any route that starts with /files/
<Route path="/files/*" element={<FileViewer />} />

function FileViewer() {
  // Get the portion of the URL that matched the splat
  const { '*': filePath } = useParams();
  
  return (
    <div>
      <h1>File Viewer</h1>
      <p>Viewing file: {filePath}</p>
      {/* Render file content based on filePath */}
    </div>
  );
}

// For URL /files/documents/report.pdf
// filePath would be "documents/report.pdf"
        

This is particularly useful for:

Real-World Example: Document Viewer

Here's how you might implement a document viewer with nested folders:


// Route definition
<Route path="/docs/*" element={<DocumentViewer />} />

// DocumentViewer component
function DocumentViewer() {
  const { '*': docPath } = useParams();
  const [document, setDocument] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    async function fetchDocument() {
      try {
        setLoading(true);
        // API call to fetch the document
        const response = await fetch(`/api/documents/${docPath}`);
        
        if (!response.ok) {
          throw new Error('Document not found');
        }
        
        const data = await response.json();
        setDocument(data);
        setError(null);
      } catch (err) {
        setError(err.message);
        setDocument(null);
      } finally {
        setLoading(false);
      }
    }
    
    fetchDocument();
  }, [docPath]);
  
  // Render loading state
  if (loading) {
    return <div>Loading document...</div>;
  }
  
  // Render error state
  if (error) {
    return <div>Error: {error}</div>;
  }
  
  // Render document
  return (
    <div className="document-viewer">
      <div className="document-path">
        <strong>Path:</strong> /docs/{docPath}
      </div>
      
      <h1>{document.title}</h1>
      
      {document.type === 'folder' ? (
        <div className="folder-contents">
          <h2>Folder Contents</h2>
          <ul>
            {document.contents.map(item => (
              <li key={item.name}>
                <Link to={`/docs/${docPath}/${item.name}`}>
                  {item.name} ({item.type})
                </Link>
              </li>
            ))}
          </ul>
        </div>
      ) : (
        <div className="document-content">
          <div dangerouslySetInnerHTML={{ __html: document.content }} />
        </div>
      )}
    </div>
  );
}
          

This component would handle URLs like:

  • /docs/development (a folder)
  • /docs/development/react (a subfolder)
  • /docs/development/react/routing.md (a document)

All using a single route definition and component!

Route Priority and Order

The order of routes in React Router matters. Routes are matched from top to bottom, and the first match is rendered.

flowchart TD URL[URL: /products/special] URL --> Route1["/products/special"] URL --> Route2["/products/:productId"] URL --> Route3["/products/*"] Route1 -->|First match wins| Special[SpecialProducts Component] Route2 -->|Not checked if above matches| Product[ProductDetail Component] Route3 -->|Not checked if above matches| Wildcard[ProductCatchAll Component] classDef route fill:#ffd,stroke:#aa3,stroke-width:2px; classDef component fill:#dfd,stroke:#3a3,stroke-width:2px; class Route1,Route2,Route3 route; class Special,Product,Wildcard component;

It's important to order your routes carefully, especially when you have:


<Routes>
  {/* Specific static routes should come first */}
  <Route path="/products/new" element={<NewProduct />} />
  <Route path="/products/special-offers" element={<SpecialOffers />} />
  
  {/* Then dynamic routes */}
  <Route path="/products/:productId" element={<ProductDetail />} />
  
  {/* Finally, catch-all or wildcard routes */}
  <Route path="/products/*" element={<ProductCatchAll />} />
</Routes>
        

With this ordering:

If we reversed the order, the wildcard route would match everything, and the specific routes would never be reached!

Common Mistake: Route Ordering

One of the most common mistakes with React Router is improper route ordering. Consider this example:


// Incorrect ordering
<Routes>
  <Route path="/products/*" element={<ProductCatchAll />} />
  <Route path="/products/:productId" element={<ProductDetail />} />
  <Route path="/products/new" element={<NewProduct />} />
</Routes>
          

With this incorrect ordering:

  • /products/new would match the wildcard route (not the specific "new" route)
  • /products/123 would also match the wildcard route (not the dynamic route)

Always order routes from most specific to least specific (with wildcards last)!

Advanced Parameter Handling

Beyond simple path parameters, React Router provides ways to handle more complex parameter scenarios.

Custom Parameter Parsing

By default, URL parameters are parsed as strings. However, you can implement custom parsing for parameters, such as converting a string ID to a number:


function ProductDetail() {
  const { productId } = useParams();
  
  // Convert string ID to number
  const id = parseInt(productId, 10);
  
  // Check for invalid IDs
  if (isNaN(id)) {
    return <div>Invalid product ID</div>;
  }
  
  // Rest of the component...
}
        

Parameter Validation

It's a good practice to validate parameters to ensure they match expected formats:


function UserProfile() {
  const { userId } = useParams();
  const navigate = useNavigate();
  
  // Validate UUID format
  const isValidUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId);
  
  useEffect(() => {
    // Redirect to 404 page if invalid UUID
    if (!isValidUUID) {
      navigate('/not-found', { replace: true });
    }
  }, [userId, isValidUUID, navigate]);
  
  if (!isValidUUID) {
    return null; // Will redirect
  }
  
  // Rest of the component...
}
        

Multiple Parameter Formats

Sometimes you need to support multiple URL formats for the same resource. You can handle this with separate routes pointing to the same component:


// Support both numeric IDs and slugs
<Route path="/products/:productId(\d+)" element={<ProductDetail />} />
<Route path="/products/:productSlug" element={<ProductDetail />} />

function ProductDetail() {
  const params = useParams();
  const productId = params.productId;
  const productSlug = params.productSlug;
  
  // Determine which parameter was used
  const isIdRoute = !!productId;
  
  useEffect(() => {
    if (isIdRoute) {
      // Fetch by ID
      fetchProductById(productId);
    } else {
      // Fetch by slug
      fetchProductBySlug(productSlug);
    }
  }, [isIdRoute, productId, productSlug]);
  
  // Rest of the component...
}
        

Real-World Example: E-commerce Product URLs

Many e-commerce sites support multiple URL formats for products to improve SEO and user experience:


// in your routes config
<Routes>
  {/* URL with category, product name, and ID for SEO */}
  <Route 
    path="/shop/:category/:productName-:productId" 
    element={<ProductDetail />} 
  />
  
  {/* Direct ID access for sharing and compatibility */}
  <Route 
    path="/products/:productId" 
    element={<ProductDetail />} 
  />
</Routes>

// ProductDetail.js
function ProductDetail() {
  const params = useParams();
  const { 
    productId, 
    category, 
    productName 
  } = params;
  
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();
  
  useEffect(() => {
    async function fetchProduct() {
      setLoading(true);
      try {
        const response = await fetch(`/api/products/${productId}`);
        const data = await response.json();
        setProduct(data);
        
        // If we accessed via the direct route and product exists,
        // redirect to the SEO-friendly URL
        if (!category && !productName && data) {
          const slug = data.name
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, '-');
            
          navigate(
            `/shop/${data.category}/${slug}-${data.id}`,
            { replace: true }
          );
        }
      } catch (error) {
        console.error('Error fetching product:', error);
      } finally {
        setLoading(false);
      }
    }
    
    fetchProduct();
  }, [productId, category, productName, navigate]);
  
  if (loading) {
    return <div>Loading...</div>;
  }
  
  if (!product) {
    return <div>Product not found</div>;
  }
  
  return (
    <div>
      {/* Product detail content */}
    </div>
  );
}
          

This approach supports both URLs:

  • /shop/electronics/samsung-galaxy-s21-123 (SEO-friendly)
  • /products/123 (direct access)

And automatically redirects from the direct URL to the SEO-friendly one when possible.

Route Objects and Configuration

For larger applications, defining routes directly in JSX can become unwieldy. React Router allows you to define routes as JavaScript objects, which can be more maintainable and enables dynamic route generation.

Creating Route Objects

You can define your routes as JavaScript objects and then use the useRoutes hook or the createBrowserRouter function:


// routes.js
import Home from './pages/Home';
import About from './pages/About';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import NotFound from './pages/NotFound';

const routes = [
  {
    path: '/',
    element: <Home />
  },
  {
    path: '/about',
    element: <About />
  },
  {
    path: '/products',
    element: <Products />
  },
  {
    path: '/products/:productId',
    element: <ProductDetail />
  },
  {
    path: '*',
    element: <NotFound />
  }
];

export default routes;

// App.js with useRoutes
import { BrowserRouter, useRoutes } from 'react-router-dom';
import routes from './routes';

function RouteRenderer() {
  // useRoutes takes an array of route objects and returns a React element
  return useRoutes(routes);
}

function App() {
  return (
    <BrowserRouter>
      <div>
        <Navbar />
        <main>
          <RouteRenderer />
        </main>
      </div>
    </BrowserRouter>
  );
}
        

Dynamic Route Generation

Using JavaScript objects for routes allows you to generate routes dynamically based on data:


// Dynamically generate routes for each product category
const categories = ['electronics', 'clothing', 'books', 'home', 'toys'];

const categoryRoutes = categories.map(category => ({
  path: `/category/${category}`,
  element: <CategoryPage category={category} />
}));

const routes = [
  { path: '/', element: <Home /> },
  { path: '/products', element: <Products /> },
  // Spread the category routes
  ...categoryRoutes,
  { path: '*', element: <NotFound /> }
];
        

Route Configuration with Data Loading

In React Router v6.4+, you can use the Data API with createBrowserRouter to specify data loading logic for each route:


// routes.js
import { createBrowserRouter } from 'react-router-dom';

// Define loaders and actions for routes
async function productsLoader() {
  const response = await fetch('/api/products');
  if (!response.ok) {
    throw new Error('Failed to load products');
  }
  return await response.json();
}

async function productLoader({ params }) {
  const response = await fetch(`/api/products/${params.productId}`);
  if (!response.ok) {
    throw new Error('Failed to load product');
  }
  return await response.json();
}

async function submitContactForm({ request }) {
  const formData = await request.formData();
  const response = await fetch('/api/contact', {
    method: 'POST',
    body: formData
  });
  
  if (!response.ok) {
    throw new Error('Failed to submit form');
  }
  
  return await response.json();
}

// Create the router with data loading
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        path: '/',
        element: <Home />
      },
      {
        path: '/products',
        element: <Products />,
        loader: productsLoader
      },
      {
        path: '/products/:productId',
        element: <ProductDetail />,
        loader: productLoader
      },
      {
        path: '/contact',
        element: <Contact />,
        action: submitContactForm
      }
    ]
  }
]);

export default router;

// main.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import router from './routes';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);
        

In the components, you can access the loaded data using the useLoaderData hook:


import { useLoaderData } from 'react-router-dom';

function Products() {
  // This data comes from the productsLoader
  const products = useLoaderData();
  
  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <Link to={`/products/${product.id}`}>
              {product.name}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}
        

When to Use Route Objects vs. JSX Routes

Both approaches have their merits:

JSX Routes Route Objects
More declarative and React-like Better for dynamic route generation
Easier to understand for React developers Better for large applications with many routes
Routes are defined where they're used Routes can be defined separately and imported
Good for smaller applications Easier to manipulate programmatically

Choose based on your application's size and complexity. For larger applications or when you need dynamic route generation, route objects typically work better.

Route Guards and Protected Routes

Many applications need to restrict access to certain routes based on authentication status or user permissions. This is commonly achieved with route guards.

Basic Authentication Guards

A common pattern is to create a ProtectedRoute component that checks if the user is authenticated before rendering the desired component:


// components/ProtectedRoute.js
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  const location = useLocation();
  
  if (!user) {
    // Redirect to login page if not authenticated
    // Save the current location to redirect back after login
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  // User is authenticated, render the protected component
  return children;
}

export default ProtectedRoute;

// Usage in routes
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/login" element={<Login />} />
  <Route 
    path="/dashboard" 
    element={
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    } 
  />
  <Route 
    path="/profile" 
    element={
      <ProtectedRoute>
        <UserProfile />
      </ProtectedRoute>
    } 
  />
</Routes>
        

Role-Based Route Guards

You can extend the protected route concept to include role-based access control:


// components/RoleRoute.js
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function RoleRoute({ children, requiredRoles }) {
  const { user } = useAuth();
  const location = useLocation();
  
  // Check if user exists
  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  // Check if user has any of the required roles
  const hasRequiredRole = requiredRoles.some(role => 
    user.roles.includes(role)
  );
  
  if (!hasRequiredRole) {
    // User doesn't have the required role, redirect to unauthorized page
    return <Navigate to="/unauthorized" replace />;
  }
  
  // User has a required role, render the protected component
  return children;
}

export default RoleRoute;

// Usage in routes
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/login" element={<Login />} />
  <Route path="/unauthorized" element={<Unauthorized />} />
  
  {/* User-accessible routes */}
  <Route 
    path="/dashboard" 
    element={
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    } 
  />
  
  {/* Admin-only routes */}
  <Route 
    path="/admin" 
    element={
      <RoleRoute requiredRoles={['admin']}>
        <AdminDashboard />
      </RoleRoute>
    } 
  />
  
  {/* Multiple role access */}
  <Route 
    path="/reports" 
    element={
      <RoleRoute requiredRoles={['admin', 'analyst']}>
        <Reports />
      </RoleRoute>
    } 
  />
</Routes>
        

Using Auth Guards with Route Objects

If you're using route objects, you can implement protection with a higher-order component or wrapper function:


// Higher-order component to protect a route
function withAuth(Component) {
  return function ProtectedComponent(props) {
    const { user } = useAuth();
    const location = useLocation();
    
    if (!user) {
      return <Navigate to="/login" state={{ from: location }} replace />;
    }
    
    return <Component {...props} />;
  };
}

// Routes with protection
const routes = [
  { path: '/', element: <Home /> },
  { path: '/login', element: <Login /> },
  { path: '/dashboard', element: withAuth(Dashboard) },
  { path: '/profile', element: withAuth(UserProfile) }
];
        

Real-World Example: Multi-Level Auth with Layout Routes

In a real application, you might want different layouts for public vs. authenticated sections:


// Route configuration
import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom';
import { useAuth } from './contexts/AuthContext';

// Layout components
function PublicLayout() {
  return (
    <div>
      <PublicHeader />
      <Outlet /> {/* Child routes render here */}
      <PublicFooter />
    </div>
  );
}

function AuthLayout() {
  const { user } = useAuth();
  
  // Redirect to login if not authenticated
  if (!user) {
    return <Navigate to="/login" replace />;
  }
  
  return (
    <div>
      <AuthHeader user={user} />
      <Sidebar />
      <main>
        <Outlet /> {/* Child routes render here */}
      </main>
    </div>
  );
}

function AdminLayout() {
  const { user } = useAuth();
  
  // Check for admin role
  if (!user || !user.roles.includes('admin')) {
    return <Navigate to="/unauthorized" replace />;
  }
  
  return (
    <div>
      <AdminHeader user={user} />
      <AdminSidebar />
      <main>
        <Outlet /> {/* Child routes render here */}
      </main>
    </div>
  );
}

// Create router with nested layouts
const router = createBrowserRouter([
  // Public routes
  {
    element: <PublicLayout />,
    children: [
      { path: '/', element: <Home /> },
      { path: '/login', element: <Login /> },
      { path: '/register', element: <Register /> },
      { path: '/about', element: <About /> },
      { path: '/unauthorized', element: <Unauthorized /> }
    ]
  },
  
  // Authenticated user routes
  {
    element: <AuthLayout />,
    children: [
      { path: '/dashboard', element: <Dashboard /> },
      { path: '/profile', element: <Profile /> },
      { path: '/settings', element: <Settings /> },
      { path: '/orders', element: <Orders /> }
    ]
  },
  
  // Admin routes
  {
    element: <AdminLayout />,
    children: [
      { path: '/admin', element: <AdminDashboard /> },
      { path: '/admin/users', element: <UserManagement /> },
      { path: '/admin/products', element: <ProductManagement /> },
      { path: '/admin/settings', element: <AdminSettings /> }
    ]
  },
  
  // Catch-all route
  { path: '*', element: <NotFound /> }
]);

export default router;
          

This approach provides:

  • Different layouts for different sections (public, user, admin)
  • Protection at the layout level, so all child routes inherit it
  • Clean separation of concerns
  • Consistent UI for each section

Route Metadata and Custom Route Definitions

When using route objects, you can attach custom metadata to routes for various purposes, such as page titles, breadcrumb generation, or access control.


// routes.js with metadata
const routes = [
  {
    path: '/',
    element: <Home />,
    meta: {
      title: 'Home',
      requiresAuth: false,
      breadcrumb: 'Home'
    }
  },
  {
    path: '/products',
    element: <Products />,
    meta: {
      title: 'Products',
      requiresAuth: false,
      breadcrumb: 'Products'
    }
  },
  {
    path: '/products/:productId',
    element: <ProductDetail />,
    meta: {
      title: 'Product Details',
      requiresAuth: false,
      breadcrumb: 'Product Details'
    }
  },
  {
    path: '/dashboard',
    element: <Dashboard />,
    meta: {
      title: 'Dashboard',
      requiresAuth: true,
      breadcrumb: 'Dashboard'
    }
  }
];

// useRoutes doesn't natively use meta, so we need a wrapper
function RoutesWithMeta() {
  const routeElements = useRoutes(routes);
  
  return routeElements;
}

// PageTitle component to use route metadata
function PageTitle() {
  const location = useLocation();
  const currentRoute = routes.find(route => 
    matchPath(route.path, location.pathname)
  );
  
  useEffect(() => {
    if (currentRoute?.meta?.title) {
      document.title = `${currentRoute.meta.title} - My App`;
    } else {
      document.title = 'My App';
    }
  }, [currentRoute]);
  
  return null;
}

// Breadcrumbs component using route metadata
function Breadcrumbs() {
  const location = useLocation();
  const currentRoute = routes.find(route => 
    matchPath(route.path, location.pathname)
  );
  
  if (!currentRoute?.meta?.breadcrumb) {
    return null;
  }
  
  return (
    <div className="breadcrumbs">
      <Link to="/">Home</Link> /
      <span>{currentRoute.meta.breadcrumb}</span>
    </div>
  );
}

// App component
function App() {
  return (
    <BrowserRouter>
      <PageTitle />
      <Breadcrumbs />
      <RoutesWithMeta />
    </BrowserRouter>
  );
}
        

Custom Route Components

You can also create custom route components to handle specific behaviors:


// components/AnalyticsRoute.js
import { Route } from 'react-router-dom';

function AnalyticsRoute({ element, trackingId, ...rest }) {
  // Enhanced element that tracks page views
  const enhancedElement = (
    <PageViewTracker trackingId={trackingId}>
      {element}
    </PageViewTracker>
  );
  
  return <Route {...rest} element={enhancedElement} />;
}

// Usage
<Routes>
  <AnalyticsRoute 
    path="/" 
    element={<Home />} 
    trackingId="home-page" 
  />
  <AnalyticsRoute 
    path="/products" 
    element={<Products />} 
    trackingId="products-page" 
  />
</Routes>
        

Real-World Example: Complete Page Configuration

Here's how you might configure routes with comprehensive metadata:


// routes.js
const routes = [
  {
    path: '/',
    element: <Home />,
    meta: {
      title: 'Home',
      description: 'Welcome to our e-commerce store',
      requiresAuth: false,
      roles: [],
      breadcrumb: 'Home',
      analyticsId: 'home-page',
      preload: true,
      layout: 'standard'
    }
  },
  {
    path: '/products',
    element: <Products />,
    meta: {
      title: 'Products',
      description: 'Browse our product catalog',
      requiresAuth: false,
      roles: [],
      breadcrumb: 'Products',
      analyticsId: 'products-list',
      preload: true,
      layout: 'standard'
    }
  },
  {
    path: '/dashboard',
    element: <Dashboard />,
    meta: {
      title: 'User Dashboard',
      description: 'Manage your account and orders',
      requiresAuth: true,
      roles: ['user'],
      breadcrumb: 'Dashboard',
      analyticsId: 'user-dashboard',
      preload: false,
      layout: 'dashboard'
    }
  },
  {
    path: '/admin',
    element: <AdminPanel />,
    meta: {
      title: 'Admin Panel',
      description: 'Site administration',
      requiresAuth: true,
      roles: ['admin'],
      breadcrumb: 'Admin',
      analyticsId: 'admin-panel',
      preload: false,
      layout: 'admin'
    },
    // Nested routes
    children: [
      {
        path: 'users',
        element: <UserManagement />,
        meta: {
          title: 'User Management',
          description: 'Manage user accounts',
          requiresAuth: true,
          roles: ['admin'],
          breadcrumb: 'Users',
          analyticsId: 'admin-users',
          preload: false,
          layout: 'admin'
        }
      }
    ]
  }
];

// RouteRenderer that applies all the metadata behavior
function RouteRenderer() {
  const { user } = useAuth();
  const location = useLocation();
  
  // Function to find the current route
  const findActiveRoute = (pathname, routesList, parent = null) => {
    for (const route of routesList) {
      const pattern = parent 
        ? `${parent.path}/${route.path}`.replace(/\/+/g, '/') 
        : route.path;
        
      const match = matchPath(pattern, pathname);
      
      if (match) {
        return { route, match };
      }
      
      if (route.children) {
        const childMatch = findActiveRoute(pathname, route.children, route);
        if (childMatch) return childMatch;
      }
    }
    
    return null;
  };
  
  const activeRoute = findActiveRoute(location.pathname, routes);
  
  // Set document metadata
  useEffect(() => {
    if (activeRoute?.route?.meta) {
      const { title, description } = activeRoute.route.meta;
      
      if (title) {
        document.title = `${title} - My App`;
      }
      
      // Update meta description
      let metaDescription = document.querySelector('meta[name="description"]');
      if (!metaDescription) {
        metaDescription = document.createElement('meta');
        metaDescription.name = 'description';
        document.head.appendChild(metaDescription);
      }
      
      if (description) {
        metaDescription.content = description;
      }
    }
  }, [activeRoute]);
  
  // Track page view
  useEffect(() => {
    if (activeRoute?.route?.meta?.analyticsId) {
      analytics.trackPageView(activeRoute.route.meta.analyticsId);
    }
  }, [activeRoute]);
  
  // Auth checking function
  const checkAuth = (route) => {
    if (!route.meta) return true;
    
    if (route.meta.requiresAuth && !user) {
      return false;
    }
    
    if (route.meta.roles && route.meta.roles.length > 0) {
      if (!user) return false;
      
      const hasRequiredRole = route.meta.roles.some(
        role => user.roles.includes(role)
      );
      
      if (!hasRequiredRole) return false;
    }
    
    return true;
  };
  
  // Custom rendering function
  const renderRoutes = (routesList, parent = null) => {
    return routesList.map(route => {
      const path = parent 
        ? `${parent.path}/${route.path}`.replace(/\/+/g, '/') 
        : route.path;
        
      const isAuthorized = checkAuth(route);
      
      // Determine which layout to use
      const LayoutComponent = layouts[route.meta?.layout || 'standard'] || Fragment;
      
      // Apply authorization and layout
      const wrappedElement = (
        <LayoutComponent>
          {isAuthorized ? route.element : <Navigate to="/login" replace />}
        </LayoutComponent>
      );
      
      // Recursively handle nested routes
      if (route.children) {
        return (
          <Route key={path} path={path} element={wrappedElement}>
            {renderRoutes(route.children, route)}
          </Route>
        );
      }
      
      return (
        <Route key={path} path={path} element={wrappedElement} />
      );
    });
  };
  
  return <Routes>{renderRoutes(routes)}</Routes>;
}
          

This advanced setup provides:

  • SEO management (title, description)
  • Authentication and authorization checks
  • Analytics tracking
  • Dynamic layouts based on route metadata
  • Breadcrumb generation
  • Support for nested routes

Practice Activities

Activity 1: Advanced Route Parameters

Create a blog application with the following routes:

  1. Home page that lists all blog posts
  2. Blog post detail page using a slug parameter (/blog/my-first-post)
  3. Support both slug and ID formats for posts (/blog/123 should also work)
  4. Author page that shows all posts by an author (/author/john-doe)
  5. Category page that shows all posts in a category (/category/technology)

Make sure to:

Activity 2: Route Guards and Authentication

Extend your blog application with authentication and route protection:

  1. Add login and registration pages
  2. Create a user dashboard that's only accessible to logged-in users
  3. Add an admin section that's only accessible to users with the "admin" role
  4. Implement different layouts for public, user, and admin sections
  5. Add a profile page that shows the logged-in user's information

Implement route guards to protect the appropriate routes.

Activity 3: Route Configuration and Metadata

Refactor your blog application to use route objects and metadata:

  1. Convert all routes to use route objects instead of JSX
  2. Add metadata to each route (title, description, etc.)
  3. Create components that use this metadata (PageTitle, MetaTags, Breadcrumbs)
  4. Implement analytics tracking based on route metadata
  5. Create a route visualization component that shows the site hierarchy

Summary

Further Resources