Doing React the Right Way - 10/10

Application Architecture and Routing: Building for Scale

You’ve mastered React’s core concepts, advanced patterns, and modern features. You can build components, manage state, handle side effects, and optimize performance. Now it’s time to architect complete applications that scale beyond individual components to cohesive, maintainable systems.

This final article covers project organization, client-side routing, and architectural patterns that ensure your applications remain manageable as they grow in complexity and team size.

Project Structure: Feature-Driven Organization

How you organize your code determines how easy it is to navigate, maintain, and extend your application. The wrong structure creates friction; the right structure makes development feel effortless.

The Problem with Type-Based Organization

Many developers start by organizing files by their technical type:

src/
├── components/
│   ├── Header.tsx
│   ├── ProductCard.tsx
│   └── UserProfile.tsx
├── hooks/
│   ├── useAuth.ts
│   └── useProducts.ts
├── pages/
│   ├── HomePage.tsx
│   └── ProductsPage.tsx
└── utils/
    └── api.ts

While this seems logical, it creates problems as applications grow:

Cross-Directory Navigation: Working on the “product” feature requires jumping between multiple directories (components, hooks, pages), making it hard to see the complete feature scope.

Unclear Boundaries: It’s difficult to understand which components belong to which features or how they relate to each other.

Merge Conflicts: Multiple developers working on different features often modify the same directories, leading to git conflicts.

Dead Code Detection: It’s hard to identify unused code because dependencies span multiple directories.

Feature-Driven Architecture

The solution is organizing code by business features rather than technical types:

src/
├── app/                    # Application shell and global configuration
│   ├── providers/          # Context providers (Theme, Auth, Router)
│   │   ├── AuthProvider.tsx
│   │   └── ThemeProvider.tsx
│   ├── layout/             # Global layout components
│   │   ├── AppLayout.tsx
│   │   └── Navigation.tsx
│   └── styles/             # Global styles and design system
│       ├── globals.css
│       └── variables.css
│
├── shared/                 # Truly reusable, generic code
│   ├── components/         # Generic UI components
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.module.css
│   │   │   └── index.ts
│   │   ├── Input/
│   │   └── Modal/
│   ├── hooks/              # Generic hooks (useDebounce, useLocalStorage)
│   │   ├── useDebounce.ts
│   │   └── useLocalStorage.ts
│   ├── utils/              # Pure utility functions
│   │   ├── formatters.ts
│   │   └── validators.ts
│   └── types/              # Global TypeScript types
│       └── api.ts
│
├── features/               # Business features (the heart of your app)
│   ├── authentication/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   ├── SignupForm.tsx
│   │   │   └── UserMenu.tsx
│   │   ├── hooks/
│   │   │   ├── useAuth.ts
│   │   │   └── useLogin.ts
│   │   ├── services/
│   │   │   └── authApi.ts
│   │   └── index.ts        # Public API of the feature
│   │
│   ├── product-catalog/
│   │   ├── components/
│   │   │   ├── ProductCard.tsx
│   │   │   ├── ProductGrid.tsx
│   │   │   └── ProductFilters.tsx
│   │   ├── hooks/
│   │   │   ├── useProducts.ts
│   │   │   └── useProductSearch.ts
│   │   ├── services/
│   │   │   └── productsApi.ts
│   │   └── index.ts
│   │
│   └── shopping-cart/
│       ├── components/
│       ├── hooks/
│       ├── services/
│       └── index.ts
│
├── pages/                  # Route components that compose features
│   ├── HomePage.tsx
│   ├── ProductsPage.tsx
│   └── CheckoutPage.tsx
│
└── main.tsx               # Application entry point

Benefits of Feature-Driven Organization

Logical Grouping: Everything related to a feature lives in one place, making it easy to understand the feature’s complete scope.

Clear Ownership: Teams can own entire feature directories, reducing coordination overhead.

Easy Deletion: Removing a feature means deleting one directory, with minimal impact on other features.

Scalable Structure: New features get their own directories without affecting existing code organization.

Reduced Coupling: Features are naturally isolated, encouraging better architectural boundaries.

Feature Index Files

Each feature should export its public API through an index file:

// features/authentication/index.ts
export { LoginForm } from "./components/LoginForm";
export { UserMenu } from "./components/UserMenu";
export { useAuth } from "./hooks/useAuth";
export type { User, AuthState } from "./types";

// Don't export internal components or utilities
// This keeps the feature's API clean and prevents tight coupling

This allows other parts of the application to import from the feature cleanly:

// pages/HomePage.tsx
import { UserMenu } from "@/features/authentication";
import { ProductGrid } from "@/features/product-catalog";

function HomePage() {
  return (
    <div>
      <UserMenu />
      <ProductGrid />
    </div>
  );
}

Client-Side Routing with React Router

Single Page Applications create the illusion of multiple pages through client-side routing. React Router is the standard solution for managing navigation, URL synchronization, and nested layouts.

Modern Router Configuration

React Router v6.4+ uses a configuration-based approach that’s more powerful and easier to reason about:

// main.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

// Import layouts and pages
import AppLayout from "@/app/layout/AppLayout";
import HomePage from "@/pages/HomePage";
import ProductsPage from "@/pages/ProductsPage";
import ProductDetailPage from "@/pages/ProductDetailPage";
import CheckoutPage from "@/pages/CheckoutPage";
import ErrorPage from "@/pages/ErrorPage";

const router = createBrowserRouter([
  {
    path: "/",
    element: <AppLayout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true, // Default route for '/'
        element: <HomePage />,
      },
      {
        path: "products",
        children: [
          {
            index: true, // '/products'
            element: <ProductsPage />,
          },
          {
            path: ":productId", // '/products/:productId'
            element: <ProductDetailPage />,
          },
        ],
      },
      {
        path: "checkout",
        element: <CheckoutPage />,
      },
    ],
  },
]);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
);

Layout Components with Outlet

The <Outlet /> component creates nested routing by rendering matched child routes:

// app/layout/AppLayout.tsx
import { Outlet } from "react-router-dom";
import { Navigation } from "./Navigation";
import { Footer } from "./Footer";

export default function AppLayout() {
  return (
    <div className="app-layout">
      <Navigation />

      <main className="main-content">
        {/* Child routes render here */}
        <Outlet />
      </main>

      <Footer />
    </div>
  );
}

Essential Router Hooks

useNavigate: Programmatic navigation for form submissions and user actions:

import { useNavigate } from "react-router-dom";

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

  const handleSubmit = async (formData: FormData) => {
    try {
      await submitOrder(formData);
      navigate("/order-confirmation", {
        replace: true, // Replace current entry in history
      });
    } catch (error) {
      // Handle error
    }
  };

  return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}

useParams: Access dynamic route parameters:

// For route '/products/:productId'
import { useParams } from "react-router-dom";

function ProductDetailPage() {
  const { productId } = useParams<{ productId: string }>();

  // Use productId to fetch product data
  const { product, loading } = useProduct(productId);

  if (loading) return <div>Loading...</div>;

  return <ProductDetails product={product} />;
}

useSearchParams: Manage URL query parameters for filters and search:

import { useSearchParams } from "react-router-dom";

function ProductsPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get("category") || "";
  const sortBy = searchParams.get("sort") || "name";

  const updateFilters = (newCategory: string) => {
    setSearchParams((prev) => {
      const params = new URLSearchParams(prev);
      if (newCategory) {
        params.set("category", newCategory);
      } else {
        params.delete("category");
      }
      return params;
    });
  };

  return (
    <div>
      <CategoryFilter value={category} onChange={updateFilters} />
      <ProductGrid category={category} sortBy={sortBy} />
    </div>
  );
}

Protected Routes

Implement route protection with wrapper components:

// app/layout/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom';
import { useAuth } from '@/features/authentication';

interface ProtectedRouteProps {
  children: React.ReactNode;
}

export function ProtectedRoute({ children }: ProtectedRouteProps) {
  const { isAuthenticated, loading } = useAuth();

  if (loading) {
    return <div>Checking authentication...</div>;
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return <>{children}</>;
}

// Usage in router configuration
{
  path: 'dashboard',
  element: (
    <ProtectedRoute>
      <DashboardPage />
    </ProtectedRoute>
  ),
}

Advanced Routing Patterns

Route-Based Code Splitting

Implement lazy loading to reduce initial bundle size:

import { lazy, Suspense } from "react";

// Lazy load heavy pages
const DashboardPage = lazy(() => import("@/pages/DashboardPage"));
const AdminPage = lazy(() => import("@/pages/AdminPage"));
const ReportsPage = lazy(() => import("@/pages/ReportsPage"));

const router = createBrowserRouter([
  {
    path: "/",
    element: <AppLayout />,
    children: [
      // Eager load frequently visited pages
      { index: true, element: <HomePage /> },
      { path: "products", element: <ProductsPage /> },

      // Lazy load less common pages
      {
        path: "dashboard",
        element: (
          <Suspense fallback={<div>Loading dashboard...</div>}>
            <DashboardPage />
          </Suspense>
        ),
      },
      {
        path: "admin",
        element: (
          <Suspense fallback={<div>Loading admin panel...</div>}>
            <AdminPage />
          </Suspense>
        ),
      },
    ],
  },
]);

Data Loading Patterns

Combine routing with data fetching for better user experience:

// hooks/useRouteData.ts
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";

export function useRouteData<T>(
  fetcher: (params: Record<string, string>) => Promise<T>
) {
  const params = useParams();
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function loadData() {
      try {
        setLoading(true);
        setError(null);
        const result = await fetcher(params);

        if (!cancelled) {
          setData(result);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : "Failed to load data");
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    loadData();
    return () => {
      cancelled = true;
    };
  }, [JSON.stringify(params), fetcher]);

  return { data, loading, error };
}

// Usage
function ProductDetailPage() {
  const {
    data: product,
    loading,
    error,
  } = useRouteData(({ productId }) => fetchProduct(productId!));

  if (loading) return <ProductDetailSkeleton />;
  if (error) return <ErrorMessage error={error} />;
  if (!product) return <ProductNotFound />;

  return <ProductDetails product={product} />;
}

State Management Architecture

For complex applications, establish clear patterns for state management:

Local vs Global State

Local State (useState, useReducer):

  • Component-specific UI state
  • Form data
  • Temporary interaction state

Global State (Context, external libraries):

  • User authentication
  • App-wide settings (theme, language)
  • Cross-component shared data

Context Organization

Structure contexts by domain and update frequency:

// app/providers/AppProviders.tsx
import { AuthProvider } from "@/features/authentication";
import { ThemeProvider } from "@/app/providers/ThemeProvider";
import { NotificationProvider } from "@/app/providers/NotificationProvider";

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <NotificationProvider>{children}</NotificationProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

// main.tsx
root.render(
  <StrictMode>
    <AppProviders>
      <RouterProvider router={router} />
    </AppProviders>
  </StrictMode>
);

Production Considerations

Error Boundaries Strategy

Implement strategic error boundaries to contain failures:

// app/layout/AppLayout.tsx
import { ErrorBoundary } from "@/shared/components/ErrorBoundary";

export default function AppLayout() {
  return (
    <div className="app-layout">
      <ErrorBoundary fallback={<div>Navigation unavailable</div>}>
        <Navigation />
      </ErrorBoundary>

      <main className="main-content">
        <ErrorBoundary
          fallback={<div>Page content unavailable</div>}
          onError={(error) => reportError(error)}
        >
          <Outlet />
        </ErrorBoundary>
      </main>

      <Footer />
    </div>
  );
}

Performance Monitoring

Implement performance tracking for routing:

// utils/analytics.ts
export function trackPageView(path: string) {
  // Send to analytics service
  analytics.track("page_view", { path });
}

export function trackRouteChange(from: string, to: string, duration: number) {
  analytics.track("route_change", { from, to, duration });
}

// App.tsx
function App() {
  const location = useLocation();

  useEffect(() => {
    trackPageView(location.pathname);
  }, [location.pathname]);

  return <RouterProvider router={router} />;
}

The Complete Developer Journey

You’ve now completed a comprehensive journey through modern React development:

Fundamentals: Components, JSX, props, and the declarative paradigm that makes React powerful.

State Management: From useState to complex patterns with useReducer, useContext, and optimistic updates.

Side Effects: Understanding useEffect, data fetching patterns, and integration with external systems.

Performance: Optimization techniques, memoization, and code splitting for scalable applications.

Advanced Patterns: Custom hooks, error boundaries, portals, and composition patterns for complex requirements.

Modern React: Server Components, React 19 features, and the evolving landscape of React development.

Architecture: Project organization, routing, and patterns that scale from prototypes to production applications.

The Continuous Learning Path

React continues evolving, and your learning journey continues beyond this series:

Stay Current: Follow React’s official blog, RFCs, and community discussions to understand upcoming changes.

Build Projects: Apply these concepts in real applications, from personal projects to professional work.

Contribute: Participate in open source projects to deepen your understanding and give back to the community.

Teach Others: Explaining concepts to others reinforces your own understanding and helps the community grow.

Experiment: Try new patterns, libraries, and approaches to understand their trade-offs.

Final Thoughts

React isn’t just a library—it’s a philosophy about building user interfaces that emphasizes predictability, composition, and developer experience. The patterns you’ve learned represent years of community experimentation and refinement.

Your journey from understanding basic components to architecting complete applications represents more than learning a framework. You’ve developed the ability to think systematically about user interfaces, understand the trade-offs between different approaches, and make informed architectural decisions.

The technology will continue evolving, but the fundamental principles—understanding your users’ needs, building maintainable systems, and creating delightful experiences—remain constant.

You’re now equipped not just to use React, but to use it thoughtfully, effectively, and professionally. The applications you build will be better for it, and the users who depend on them will benefit from your careful consideration of performance, accessibility, and user experience.

Welcome to the community of developers who understand that great software is built through careful craft, continuous learning, and respect for both the technology and the people who use it.

The foundation is complete. Now go build something amazing.