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.