Route Configuration Basics

Building Structured Navigation Systems

Welcome to Route Configuration!

Think of route configuration like creating a blueprint for a building. Each route is a room, and the configuration tells us how these rooms connect, who can access them, and what happens when someone enters. Today, we'll learn how to create sophisticated routing structures that scale with your application.

graph TD A[Root Route /] --> B[Public Routes] A --> C[Protected Routes] A --> D[Admin Routes] B --> E[Home] B --> F[About] B --> G[Products] C --> H[Dashboard] C --> I[Profile] D --> J[User Management] D --> K[Analytics] style A fill:#ff9999 style C fill:#99ff99 style D fill:#9999ff

Route Configuration Patterns

1. Object-Based Route Configuration

// Creating routes as objects for better organization
const routes = [
  {
    path: '/',
    element: <MainLayout />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'products',
        element: <Products />,
        children: [
          {
            index: true,
            element: <ProductList />,
          },
          {
            path: ':productId',
            element: <ProductDetail />,
            loader: ({ params }) => fetchProduct(params.productId),
          },
          {
            path: 'new',
            element: <CreateProduct />,
            action: async ({ request }) => {
              const formData = await request.formData();
              return createProduct(formData);
            },
          },
        ],
      },
    ],
    errorElement: <ErrorBoundary />,
  },
  {
    path: '/auth',
    element: <AuthLayout />,
    children: [
      {
        path: 'login',
        element: <Login />,
      },
      {
        path: 'register',
        element: <Register />,
      },
    ],
  },
  {
    path: '*',
    element: <NotFound />,
  },
];

// Using createBrowserRouter for advanced features
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter(routes);

function App() {
  return <RouterProvider router={router} />;
}

2. Data Router Configuration

// Advanced route configuration with data loading
const router = createBrowserRouter([
  {
    path: '/users',
    element: <UsersLayout />,
    loader: async () => {
      const users = await fetchUsers();
      return { users };
    },
    children: [
      {
        index: true,
        element: <UserList />,
        loader: async ({ parent }) => {
          const { users } = await parent;
          return { users };
        },
      },
      {
        path: ':userId',
        element: <UserProfile />,
        loader: async ({ params }) => {
          const user = await fetchUser(params.userId);
          const posts = await fetchUserPosts(params.userId);
          return { user, posts };
        },
        shouldRevalidate: ({ currentUrl, nextUrl }) => {
          // Only revalidate if the user ID changes
          return currentUrl.params.userId !== nextUrl.params.userId;
        },
      },
      {
        path: ':userId/edit',
        element: <EditUser />,
        loader: async ({ params }) => {
          const user = await fetchUser(params.userId);
          return { user };
        },
        action: async ({ request, params }) => {
          const formData = await request.formData();
          const updatedUser = await updateUser(params.userId, formData);
          return redirect(`/users/${params.userId}`);
        },
      },
    ],
  },
]);

// Using loaded data in components
import { useLoaderData } from 'react-router-dom';

function UserProfile() {
  const { user, posts } = useLoaderData();
  
  return (
    <div>
      <h1>{user.name}</h1>
      <div>
        <h2>Posts</h2>
        {posts.map(post => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>
    </div>
  );
}

Advanced Route Guards

// Creating a flexible route guard system
const createRouteGuard = (config) => {
  return ({ children }) => {
    const { user, loading } = useAuth();
    const location = useLocation();
    
    if (loading) {
      return <LoadingScreen />;
    }
    
    // Check authentication
    if (config.requireAuth && !user) {
      return <Navigate to="/login" state={{ from: location }} replace />;
    }
    
    // Check roles
    if (config.allowedRoles && (!user || !config.allowedRoles.includes(user.role))) {
      return <Navigate to="/unauthorized" replace />;
    }
    
    // Check permissions
    if (config.requiredPermissions) {
      const hasPermissions = config.requiredPermissions.every(
        permission => user?.permissions?.includes(permission)
      );
      
      if (!hasPermissions) {
        return <Navigate to="/forbidden" replace />;
      }
    }
    
    // Check custom conditions
    if (config.customCheck && !config.customCheck(user)) {
      return <Navigate to={config.fallbackPath || '/'} replace />;
    }
    
    return children;
  };
};

// Usage examples
const AuthGuard = createRouteGuard({ requireAuth: true });
const AdminGuard = createRouteGuard({ 
  requireAuth: true, 
  allowedRoles: ['admin'] 
});
const EditorGuard = createRouteGuard({ 
  requireAuth: true, 
  requiredPermissions: ['edit_content', 'publish_content'] 
});

// Custom condition example
const SubscriptionGuard = createRouteGuard({
  requireAuth: true,
  customCheck: (user) => user.subscriptionActive,
  fallbackPath: '/subscribe'
});

// Applying guards in route configuration
const routes = [
  {
    path: '/admin',
    element: <AdminGuard><AdminLayout /></AdminGuard>,
    children: [
      {
        index: true,
        element: <AdminDashboard />,
      },
      {
        path: 'users',
        element: <UserManagement />,
      },
    ],
  },
  {
    path: '/editor',
    element: <EditorGuard><EditorLayout /></EditorGuard>,
    children: [
      {
        path: 'articles',
        element: <ArticleEditor />,
      },
    ],
  },
  {
    path: '/premium',
    element: <SubscriptionGuard><PremiumContent /></SubscriptionGuard>,
  },
];

Lazy Loading Routes

// Implementing lazy loading for better performance
import { lazy, Suspense } from 'react';

// Lazy load components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const Analytics = lazy(() => import('./pages/Analytics'));

// Create a loading component
function PageLoader() {
  return (
    <div className="page-loader">
      <div className="spinner"></div>
      <p>Loading...</p>
    </div>
  );
}

// Wrapper for lazy components
function LazyBoundary({ children }) {
  return (
    <Suspense fallback={<PageLoader />}>
      {children}
    </Suspense>
  );
}

// Route configuration with lazy loading
const routes = [
  {
    path: '/dashboard',
    element: (
      <LazyBoundary>
        <Dashboard />
      </LazyBoundary>
    ),
  },
  {
    path: '/users/:userId',
    element: (
      <LazyBoundary>
        <UserProfile />
      </LazyBoundary>
    ),
  },
  {
    path: '/admin',
    element: (
      <AdminGuard>
        <LazyBoundary>
          <AdminPanel />
        </LazyBoundary>
      </AdminGuard>
    ),
    children: [
      {
        path: 'analytics',
        element: (
          <LazyBoundary>
            <Analytics />
          </LazyBoundary>
        ),
      },
    ],
  },
];

// Advanced lazy loading with preloading
const preloadComponent = (component) => {
  component.preload();
};

// Preload on hover
function PreloadLink({ to, children, component }) {
  return (
    <Link 
      to={to}
      onMouseEnter={() => preloadComponent(component)}
    >
      {children}
    </Link>
  );
}

// Usage
<PreloadLink to="/dashboard" component={Dashboard}>
  Dashboard
</PreloadLink>

Route Metadata and Breadcrumbs

// Adding metadata to routes
const routes = [
  {
    path: '/',
    element: <MainLayout />,
    handle: {
      crumb: () => 'Home',
      title: 'Home Page',
      meta: {
        description: 'Welcome to our website',
        keywords: ['home', 'main'],
      },
    },
    children: [
      {
        path: 'products',
        element: <Products />,
        handle: {
          crumb: () => 'Products',
          title: 'Our Products',
        },
        children: [
          {
            path: ':productId',
            element: <ProductDetail />,
            loader: async ({ params }) => {
              const product = await fetchProduct(params.productId);
              return { product };
            },
            handle: {
              crumb: (data) => data.product.name,
              title: (data) => `${data.product.name} | Products`,
            },
          },
        ],
      },
    ],
  },
];

// Breadcrumb component using route metadata
import { useMatches } from 'react-router-dom';

function Breadcrumbs() {
  const matches = useMatches();
  
  const crumbs = matches
    .filter(match => match.handle?.crumb)
    .map(match => ({
      path: match.pathname,
      crumb: match.handle.crumb(match.data),
    }));
  
  return (
    <nav aria-label="breadcrumb">
      <ol className="breadcrumb">
        {crumbs.map((crumb, index) => (
          <li key={crumb.path} className="breadcrumb-item">
            {index === crumbs.length - 1 ? (
              <span>{crumb.crumb}</span>
            ) : (
              <Link to={crumb.path}>{crumb.crumb}</Link>
            )}
          </li>
        ))}
      </ol>
    </nav>
  );
}

// Dynamic page title using route metadata
function DynamicTitle() {
  const matches = useMatches();
  const lastMatch = matches[matches.length - 1];
  
  useEffect(() => {
    if (lastMatch.handle?.title) {
      const title = typeof lastMatch.handle.title === 'function'
        ? lastMatch.handle.title(lastMatch.data)
        : lastMatch.handle.title;
      
      document.title = title;
    }
  }, [lastMatch]);
  
  return null;
}

Route Transitions and Animations

// Implementing route transitions
import { useLocation, useOutlet } from 'react-router-dom';
import { CSSTransition, TransitionGroup } from 'react-transition-group';

function AnimatedRoutes() {
  const location = useLocation();
  const currentOutlet = useOutlet();
  
  return (
    <TransitionGroup>
      <CSSTransition
        key={location.pathname}
        timeout={300}
        classNames="page"
        unmountOnExit
      >
        <div className="page">
          {currentOutlet}
        </div>
      </CSSTransition>
    </TransitionGroup>
  );
}

// CSS for transitions
/* styles.css */
.page-enter {
  opacity: 0;
  transform: translateX(100%);
}

.page-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: opacity 300ms, transform 300ms;
}

.page-exit {
  opacity: 1;
  transform: translateX(0);
}

.page-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: opacity 300ms, transform 300ms;
}

// Using in route configuration
const routes = [
  {
    path: '/',
    element: <AnimatedRoutes />,
    children: [
      {
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
      {
        path: 'contact',
        element: <Contact />,
      },
    ],
  },
];

// Advanced: Different transitions for different routes
function AnimatedRoutesAdvanced() {
  const location = useLocation();
  const currentOutlet = useOutlet();
  
  const getTransitionClass = (pathname) => {
    if (pathname.startsWith('/dashboard')) return 'fade';
    if (pathname.startsWith('/profile')) return 'slide';
    return 'page';
  };
  
  return (
    <TransitionGroup>
      <CSSTransition
        key={location.pathname}
        timeout={300}
        classNames={getTransitionClass(location.pathname)}
        unmountOnExit
      >
        <div className="route-container">
          {currentOutlet}
        </div>
      </CSSTransition>
    </TransitionGroup>
  );
}

Route Error Boundaries

// Creating comprehensive error boundaries
import { useRouteError, isRouteErrorResponse } from 'react-router-dom';

function RootErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    // Handle different status codes
    switch (error.status) {
      case 404:
        return (
          <div className="error-page">
            <h1>404 - Not Found</h1>
            <p>The page you're looking for doesn't exist.</p>
            <Link to="/">Go Home</Link>
          </div>
        );
      case 401:
        return (
          <div className="error-page">
            <h1>Unauthorized</h1>
            <p>Please log in to access this page.</p>
            <Link to="/login">Login</Link>
          </div>
        );
      case 403:
        return (
          <div className="error-page">
            <h1>Forbidden</h1>
            <p>You don't have permission to access this page.</p>
            <Link to="/">Go Home</Link>
          </div>
        );
      default:
        return (
          <div className="error-page">
            <h1>{error.status} - {error.statusText}</h1>
            <p>{error.data?.message || 'An error occurred'}</p>
            <Link to="/">Go Home</Link>
          </div>
        );
    }
  }
  
  // Handle JavaScript errors
  return (
    <div className="error-page">
      <h1>Oops! Something went wrong</h1>
      <p>{error.message || 'Unknown error'}</p>
      {process.env.NODE_ENV === 'development' && (
        <pre>{error.stack}</pre>
      )}
      <button onClick={() => window.location.reload()}>
        Reload Page
      </button>
    </div>
  );
}

// Specialized error boundaries
function DataLoadingError() {
  const error = useRouteError();
  
  return (
    <div className="data-error">
      <h2>Failed to Load Data</h2>
      <p>{error.message}</p>
      <button onClick={() => window.location.reload()}>
        Try Again
      </button>
    </div>
  );
}

// Using error boundaries in routes
const routes = [
  {
    path: '/',
    element: <MainLayout />,
    errorElement: <RootErrorBoundary />,
    children: [
      {
        path: 'users',
        element: <Users />,
        loader: fetchUsers,
        errorElement: <DataLoadingError />,
      },
    ],
  },
];

Testing Route Configurations

// Testing routes with React Testing Library
import { render, screen } from '@testing-library/react';
import { createMemoryRouter, RouterProvider } from 'react-router-dom';

describe('Route Configuration', () => {
  test('renders home page at root path', () => {
    const router = createMemoryRouter(routes, {
      initialEntries: ['/'],
    });
    
    render(<RouterProvider router={router} />);
    
    expect(screen.getByText('Welcome Home')).toBeInTheDocument();
  });
  
  test('renders 404 page for unknown routes', () => {
    const router = createMemoryRouter(routes, {
      initialEntries: ['/unknown'],
    });
    
    render(<RouterProvider router={router} />);
    
    expect(screen.getByText('404 - Page Not Found')).toBeInTheDocument();
  });
  
  test('protects admin routes', () => {
    // Mock unauthorized user
    jest.mock('./hooks/useAuth', () => ({
      useAuth: () => ({ user: { role: 'user' }, loading: false }),
    }));
    
    const router = createMemoryRouter(routes, {
      initialEntries: ['/admin'],
    });
    
    render(<RouterProvider router={router} />);
    
    // Should redirect to unauthorized page
    expect(screen.getByText('Unauthorized')).toBeInTheDocument();
  });
  
  test('loads data correctly', async () => {
    const mockUsers = [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' },
    ];
    
    // Mock loader
    const loaderMock = jest.fn().mockResolvedValue({ users: mockUsers });
    
    const testRoutes = [
      {
        path: '/users',
        element: <UserList />,
        loader: loaderMock,
      },
    ];
    
    const router = createMemoryRouter(testRoutes, {
      initialEntries: ['/users'],
    });
    
    render(<RouterProvider router={router} />);
    
    // Wait for data to load
    expect(await screen.findByText('User 1')).toBeInTheDocument();
    expect(await screen.findByText('User 2')).toBeInTheDocument();
    expect(loaderMock).toHaveBeenCalledTimes(1);
  });
});

Advanced Route Patterns

1. Modal Routes

// Implementing modal routes
function ModalRoutes() {
  const location = useLocation();
  const navigate = useNavigate();
  
  // Check if we have a background location
  const background = location.state?.background;
  
  return (
    <>
      <Routes location={background || location}>
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<Products />} />
        <Route path="/products/:id" element={<ProductDetail />} />
      </Routes>
      
      {/* Show modal when we have a background location */}
      {background && (
        <Routes>
          <Route 
            path="/products/:id" 
            element={
              <Modal onClose={() => navigate(-1)}>
                <ProductDetail />
              </Modal>
            } 
          />
        </Routes>
      )}
    </>
  );
}

// Link with modal state
function ProductLink({ product }) {
  const location = useLocation();
  
  return (
    <Link 
      to={`/products/${product.id}`}
      state={{ background: location }}
    >
      {product.name}
    </Link>
  );
}

2. Wizard/Multi-step Routes

// Multi-step form with route validation
const wizardRoutes = [
  {
    path: '/signup',
    element: <SignupWizard />,
    children: [
      {
        index: true,
        element: <Navigate to="personal-info" replace />,
      },
      {
        path: 'personal-info',
        element: <PersonalInfoStep />,
        canActivate: () => true, // Always accessible
      },
      {
        path: 'account-details',
        element: <AccountDetailsStep />,
        canActivate: (formData) => !!formData.personalInfo,
      },
      {
        path: 'preferences',
        element: <PreferencesStep />,
        canActivate: (formData) => !!formData.personalInfo && !!formData.accountDetails,
      },
      {
        path: 'confirmation',
        element: <ConfirmationStep />,
        canActivate: (formData) => {
          return !!formData.personalInfo && 
                 !!formData.accountDetails && 
                 !!formData.preferences;
        },
      },
    ],
  },
];

function WizardGuard({ children, canActivate }) {
  const { formData } = useWizardContext();
  const navigate = useNavigate();
  
  useEffect(() => {
    if (!canActivate(formData)) {
      navigate('/signup/personal-info', { replace: true });
    }
  }, [canActivate, formData, navigate]);
  
  return canActivate(formData) ? children : null;
}

Practice Exercises

Exercise 1: Create a Dashboard Route Structure

Build a dashboard with the following features:

  • Protected routes requiring authentication
  • Role-based access (admin, editor, viewer)
  • Lazy loading for heavy components
  • Breadcrumb navigation
  • Error boundaries for each section

Exercise 2: Implement Route Transitions

Create a route configuration with:

  • Different transition animations for different routes
  • Page preloading on hover
  • Loading states during transitions

Exercise 3: Build a Route Testing Suite

Write comprehensive tests for:

  • Route rendering
  • Protected route behavior
  • Data loading
  • Error handling

Route Configuration Best Practices

1. Organize Routes by Feature

// Group related routes together
const routes = [
  {
    path: '/blog',
    children: blogRoutes,
  },
  {
    path: '/shop',
    children: shopRoutes,
  },
  {
    path: '/account',
    children: accountRoutes,
  },
];

// Separate route modules
// blog.routes.js
export const blogRoutes = [
  {
    index: true,
    element: <BlogList />,
  },
  {
    path: ':slug',
    element: <BlogPost />,
  },
  {
    path: 'categories/:category',
    element: <CategoryPosts />,
  },
];

2. Use TypeScript for Route Safety

// Type-safe route configuration
interface RouteConfig {
  path: string;
  element: React.ReactNode;
  errorElement?: React.ReactNode;
  loader?: LoaderFunction;
  action?: ActionFunction;
  children?: RouteConfig[];
  handle?: {
    crumb?: (data: any) => string;
    title?: string | ((data: any) => string);
  };
}

const typedRoutes: RouteConfig[] = [
  {
    path: '/',
    element: <Home />,
    handle: {
      crumb: () => 'Home',
      title: 'Welcome',
    },
  },
  // TypeScript will enforce proper structure
];

Key Takeaways