Architecture

11 min read
Rapid overview

React Architecture Best Practices (Interview-Ready)

Scalable React apps require intentional decisions about component design, folder structure, state management, data-fetching boundaries, and modular patterns. This guide covers the architectural principles that distinguish production-grade codebases from throwaway prototypes.

Table of Contents


Component-Based Architecture

React's core strength is its component model — build UI by composing small, reusable pieces. Each component should have a single responsibility: it either handles rendering or logic, not both.

Key principles:

  • Prefer functional components with Hooks over class components.
  • Keep components small: if a component exceeds ~200 lines, split it.
  • Co-locate tests, styles, and types with the component for easier maintenance.
Q: What makes a well-designed React component?

A: A well-designed component has a single responsibility, a clear props API, minimal internal state, and is testable in isolation. It handles either presentation or logic — not both.


Q: Why should you prefer functional components with Hooks over class components?

A: Functional components are simpler, more composable, and avoid the complexity of this binding and lifecycle method timing. Hooks enable reusing stateful logic across components without inheritance or HOCs. Class components also don't work with React Server Components.


Component Composition Rules

  1. Lift state up only as far as necessary — the nearest common ancestor.
  2. Compose via children instead of nesting component definitions inside other components (this causes remounting on every render).
  3. Extract custom hooks when logic is reused across two or more components.
  4. Use React.memo only after profiling confirms unnecessary re-renders — premature memoization adds complexity for no gain.

✅ Good example: Composition via children

function Layout({ children }) {
  return (
    <div className="layout">
      <Header />
      <main>{children}</main>
      <Footer />
    </div>
  );
}

function DashboardPage() {
  return (
    <Layout>
      <DashboardContent />
    </Layout>
  );
}

❌ Bad example: Defining components inside other components

function DashboardPage() {
  // This component is re-created every render — causes remounting
  function DashboardContent() {
    return <div>Dashboard</div>;
  }

  return (
    <Layout>
      <DashboardContent />
    </Layout>
  );
}
Q: Why should you never define a component inside another component's render body?

A: Because React creates a new component reference on every render, which causes the inner component to unmount and remount — destroying its state, losing focus, and hurting performance. Always define components at the module level.


Project & Folder Structure

Organizing source code purposefully makes large apps easier to develop and maintain. Two common approaches exist, and most mature codebases use a hybrid.

Group all files related to a feature together: components, hooks, services, types, tests.

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── LoginForm.test.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── api/
│   │   │   └── authApi.ts
│   │   ├── types.ts
│   │   └── index.ts          # public API barrel
│   ├── dashboard/
│   └── users/
├── components/                # shared/generic UI components
│   ├── Button/
│   ├── Modal/
│   └── DataTable/
├── hooks/                     # shared hooks
├── api/                       # shared API client/config
├── utils/                     # pure utility functions
├── styles/                    # global styles/themes
├── store/                     # global state (if any)
└── App.tsx

Separation by Role/Type (Simpler, for Smaller Apps)

src/
├── components/
├── pages/
├── services/
├── hooks/
├── store/
├── utils/
└── styles/
Q: When should you use feature-based folder structure vs type-based?

A: Use feature-based for medium-to-large apps (5+ features, multiple developers). It keeps related code together, making it easier to navigate and modify a single feature without touching unrelated files. Type-based is acceptable for small apps but becomes unwieldy as the codebase grows because a single feature's code is scattered across many top-level folders.


Q: What is a barrel file and when should you use one?

A: A barrel file (index.ts) re-exports the public API of a module or feature. It controls what other parts of the app can import, creating a clean boundary. Use them at feature boundaries (e.g., features/auth/index.ts) to hide internal implementation details. Avoid deep barrel chains that hurt tree-shaking and create circular dependencies.


State Management Strategy

React doesn't mandate a state solution, but choosing the right one for each type of state matters:

State TypeWhere It LivesTool
Local UI stateComponentuseState, useReducer
Shared UI stateNearest common ancestor or ContextuseContext, Zustand, Jotai
Server/cache stateData-fetching layerTanStack Query, SWR, RTK Query
URL stateBrowser URLReact Router useSearchParams
Form stateForm libraryReact Hook Form, Formik

Key rule: Keep state as close to where it's needed as possible. Don't hoist state to a global store when it's only used by two sibling components.

Q: What's the difference between server state and client state? Why does it matter?

A: Server state is data owned by the server (API responses, database records) — it's asynchronous, can become stale, and has a source of truth elsewhere. Client state is data owned by the frontend (UI toggles, form inputs, selected tabs). The distinction matters because server state needs caching, background refetching, and invalidation — concerns that Redux or useState don't handle well. Libraries like TanStack Query are purpose-built for server state.


Q: When should you use a global state management library vs Context?

A: Use Context for low-frequency updates shared across many components (theme, auth, locale). Use a dedicated library (Zustand, Redux Toolkit) when you have frequent updates to shared state, need middleware (logging, persistence), or need to select slices of state without re-rendering the entire tree. Context causes all consumers to re-render when the value changes, which is problematic for frequently-changing state.


Data & API Layer Separation

Separating UI from business logic and data fetching improves flexibility and testability.

Layer Architecture

┌─────────────────────────┐
│   UI Components         │  ← Renders props, handles user events
├─────────────────────────┤
│   Custom Hooks          │  ← Orchestrates logic, calls API layer
├─────────────────────────┤
│   API / Service Layer   │  ← HTTP calls, request/response transforms
├─────────────────────────┤
│   API Client (Axios/    │  ← Base config, interceptors, auth headers
│   fetch wrapper)        │
└─────────────────────────┘

✅ Good example: Separated layers

// api/userApi.ts — pure data fetching
export async function fetchUsers(): Promise<User[]> {
  const response = await apiClient.get('/users');
  return response.data;
}

// hooks/useUsers.ts — data orchestration
export function useUsers() {
  return useQuery({ queryKey: ['users'], queryFn: fetchUsers });
}

// components/UserList.tsx — pure rendering
export function UserList() {
  const { data: users, isLoading, error } = useUsers();
  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <ul>{users.map(u => <UserCard key={u.id} user={u} />)}</ul>;
}

❌ Bad example: Everything in one component

export function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => { setUsers(data); setLoading(false); })
      .catch(err => console.error(err));
  }, []);

  if (loading) return <div>Loading...</div>;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Q: Why should you separate API calls from components?

A: Separation enables independent testing (mock the API layer without rendering components), reuse (multiple components can call the same API function), and flexibility (swap HTTP clients or add caching without touching UI code). It also keeps components focused on rendering, making them easier to reason about.


Modular & Scalable Patterns

As the app grows, adopt higher-level architectural patterns:

Container / Presentational Pattern

Separate data-handling components ("containers" or "smart" components) from pure UI components ("presentational" or "dumb" components).

// Presentational — receives data via props, no side effects
function UserCard({ name, email, avatar }: UserCardProps) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

// Container — fetches data, passes to presentational
function UserCardContainer({ userId }: { userId: string }) {
  const { data: user } = useUser(userId);
  if (!user) return null;
  return <UserCard name={user.name} email={user.email} avatar={user.avatar} />;
}

With Hooks, the "container" is often just a custom hook rather than a wrapper component.

Atomic Design

Structure UI into hierarchical layers for reusable component libraries:

  • Atoms: Smallest units — Button, Input, Label, Icon
  • Molecules: Groups of atoms — SearchBar (Input + Button), FormField (Label + Input + Error)
  • Organisms: Complex sections — NavigationBar, UserProfile, DataTable
  • Templates: Page layouts with placeholder slots
  • Pages: Templates filled with real data
Q: What is the Container/Presentational pattern and is it still relevant with Hooks?

A: It separates data-fetching logic from rendering. Containers manage state and side effects; presentational components are pure functions of their props. With Hooks, the "container" is often replaced by a custom hook, but the underlying principle — separating concerns — remains essential. The pattern is still relevant; only the implementation mechanism has changed.


Q: What is Atomic Design and when is it useful?

A: Atomic Design organizes components into five levels: Atoms, Molecules, Organisms, Templates, and Pages. It's useful for design systems and component libraries where you need consistent, composable UI primitives. For feature-driven apps, a feature-based structure is usually more practical — you can apply Atomic principles within each feature's components/ folder.


Code Splitting & Lazy Loading

Split your bundle by route to avoid loading the entire app upfront:

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./features/dashboard/DashboardPage'));
const Settings = lazy(() => import('./features/settings/SettingsPage'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
Q: How does code splitting improve React app architecture?

A: Code splitting with React.lazy and dynamic import() breaks the app into smaller bundles loaded on demand. This reduces initial load time because users only download code for the route they visit. Combined with Suspense, it provides a declarative loading experience. Split at route boundaries first; split heavy feature modules only if needed after measuring bundle size.


Performance Architecture

Performance should be built into the architecture, not bolted on later:

  1. Memoize expensive computations with useMemo — but only after profiling confirms the cost.
  2. Stabilize callback references with useCallback when passing handlers to memoized children.
  3. Virtualize long lists with react-window or @tanstack/react-virtual instead of rendering thousands of DOM nodes.
  4. Debounce user input for search and filter operations that trigger API calls.
  5. Use React.memo on pure presentational components that receive stable primitive props and re-render too often.
Q: When should you use useMemo and useCallback?

A: Use useMemo when a computation is expensive AND runs on every render with the same inputs. Use useCallback when you pass a callback to a memoized child component and want to prevent unnecessary re-renders. Don't use them by default — they add memory overhead and complexity. Profile first, then optimize.


Tooling & Code Quality

Good architecture includes enforcing standards at the tooling level:

  • TypeScript: Provides type safety, better IDE support, and self-documenting interfaces. Define prop types explicitly instead of relying on inference for public component APIs.
  • ESLint + Prettier: Enforce consistent formatting and catch common mistakes. Use eslint-plugin-react-hooks for rules-of-hooks enforcement.
  • Custom Hooks for Reuse: Encapsulate any reusable stateful logic in custom hooks. Name them useXxx so the linter can enforce hook rules.
  • Strict Mode: Wrap the app in <React.StrictMode> during development to surface impure renders and missing cleanup.
Q: Why should you use TypeScript in React projects?

A: TypeScript catches type errors at compile time, provides autocompletion and refactoring support, and serves as living documentation for component props and API contracts. It's especially valuable in large codebases with multiple developers where props interfaces change frequently. The investment pays off in fewer runtime bugs and more confident refactoring.


Q: What is the purpose of React Strict Mode?

A: <React.StrictMode> intentionally double-invokes certain lifecycle functions (renders, effects) during development to help you find impure renders and side effects that don't clean up properly. It also warns about deprecated APIs. It has no effect in production builds. Wrapping your app in Strict Mode catches subtle bugs early — particularly around effects that forget cleanup.


Server-Side Rendering & Frameworks

For apps that need SEO, fast initial load, or server-rendered HTML, consider a React framework:

  • Next.js: Full-featured framework with SSR, SSG, ISR, API routes, and App Router (React Server Components).
  • Remix: Focuses on web standards, progressive enhancement, and nested routing with data loading co-located per route.
  • Vite + React: Client-only SPA setup with fast dev server and optimized builds. Best for internal tools and dashboards that don't need SEO.
Q: When should you use Next.js instead of a plain React SPA?

A: Use Next.js when you need server-side rendering (SEO, social sharing, fast first paint), static site generation, or API routes in a single project. For internal tools, dashboards, or apps behind authentication where SEO doesn't matter, a Vite-based SPA is simpler and avoids the complexity of server/client boundaries. Next.js adds significant architectural constraints (file-based routing, server/client component split) that are only worth adopting if you need the capabilities.