DEV Community

Munna Thakur
Munna Thakur

Posted on

The Complete React System Design & Architecture Guide (2026)

Who is this for? Frontend developers who want to go from writing code that works → writing code that scales. This covers everything from folder structure to accessibility to hooks — with real code, comparisons, and interview-ready answers.


Table of Contents

  1. Scalable Architecture — What & Why
  2. Project Mind Map — How to Think Before You Code
  3. Production-Level Folder Structure
  4. API Layer Separation
  5. Atomic Design System
  6. State Management Patterns
  7. Colocation Pattern
  8. Accessibility (WCAG) — Deep Dive
  9. Module 1: Advanced React Fundamentals
  10. Full Architecture Summary + Interview Answers

1. Scalable Architecture — What & Why

What does "Scalable" actually mean?

Scalable Architecture means: a system that does NOT slow down, break, or become impossible to maintain when users grow, features are added, or team size increases.

In React, it means your app's structure is designed so that:

  • Adding a new feature doesn't touch 10 unrelated files
  • A team of 10 can work without stepping on each other
  • Performance stays good even at 100k users
  • A new developer can understand the codebase in hours, not weeks

The Problem with "Normal" React Apps

Most beginners write React like this:

/components
  Header.jsx
  TodoList.jsx
  UserCard.jsx
  Modal.jsx
/pages
  Home.jsx
  Dashboard.jsx
/utils
  helpers.js
Enter fullscreen mode Exit fullscreen mode

This looks organized. But as the app grows:

  • TodoList.jsx makes direct API calls
  • State lives in random parent components
  • Business logic is scattered everywhere
  • You can't add a feature without breaking another

Result: The codebase becomes what engineers call "spaghetti code" — tangled, slow, and scary to touch.


Why Does Scalability Matter?

Without Scalable Architecture With Scalable Architecture
Adding a feature takes days Adding a feature takes hours
One developer's change breaks another's Teams work independently
Performance degrades with more features Performance stays consistent
Onboarding new devs takes weeks Onboarding takes days
Testing is nearly impossible Every piece can be tested in isolation

💡 Key Insight: Scalable architecture is not about using fancy tools. It's about making the right decisions early — folder structure, state ownership, API separation — so you don't pay the price later.


2. Project Mind Map — How to Think Before You Code

The biggest mistake developers make: They start writing code before thinking about architecture. Senior engineers think first, then code.

Here is the exact mental framework to use every time you start a new project:

Step 0 — Ask These 5 Questions First

Before writing a single line of code, answer:

  1. How many users? (100 vs 1 million changes everything)
  2. What type of app? (Dashboard / Real-time / Data-heavy / Simple CRUD)
  3. Team size? (Solo vs 10+ developers)
  4. Will features be added later? (Almost always yes)
  5. What are the critical flows? (Auth, Payment, Checkout — these need special care)

Your answers determine how complex your architecture should be. Over-engineering a small app is as bad as under-engineering a large one.


The Master Mind Map

                    Project Start
                         ↓
          ┌──────── Requirements Analysis ─────────┐
          ↓                                         ↓
    UI Complexity                           Data Complexity
          ↓                                         ↓
  Component Design                        State Management
          ↓                                         ↓
  Folder Structure                         API Layer Design
          ↓                                         ↓
  Performance Plan                        Testing Strategy
          ↓                                         ↓
          └────── Deployment & Monitoring ──────────┘
                   (CI/CD + Error Logs)
Enter fullscreen mode Exit fullscreen mode

Step 1 — Feature Breakdown (Most Important)

Don't think in pages. Think in features.

❌ Page-based thinking:
/home
/dashboard
/profile

✅ Feature-based thinking:
/auth       (Login, Register, Token management)
/dashboard  (Stats, Charts, Summary)
/profile    (View, Edit, Avatar upload)
/todo       (CRUD, Filters, Archive)
Enter fullscreen mode Exit fullscreen mode

Why? Because a "feature" contains all its related code — UI, logic, API calls, state — in one place. A "page" is just a container. Features are independent units.


Step 2 — State Strategy Planning

Think about your data before writing components:

Data Type Where It Lives Tool
Form input value Inside the form component useState
Modal open/close Inside modal component useState
User authentication App-wide global Redux Toolkit
Theme (dark/light) App-wide shared Context API
API data (todos, users) Server cache React Query
Derived values (counts, filters) Calculated on the fly No state needed

🔑 Golden Rule: Global state is your LAST option, not your first.


3. Production-Level Folder Structure

This is the folder structure used at companies like Airbnb, Meta, and large startups. Copy this as your base for any real project.

/src
  /app
    store.js          ← Redux store configuration
    routes.jsx         ← All app routes defined here

  /features            ← Each feature is a self-contained module
    /auth
      /components      ← UI components specific to auth
        LoginForm.jsx
        RegisterForm.jsx
      /pages
        LoginPage.jsx
        RegisterPage.jsx
      authAPI.js        ← API calls for auth
      authSlice.js      ← Redux state for auth
      index.js          ← Public exports (barrel file)

    /todo
      /components
        TodoItem.jsx
        TodoList.jsx
        TodoFilter.jsx
      /pages
        TodoPage.jsx
      todoAPI.js
      todoSlice.js
      index.js

    /dashboard
      /components
        StatsCard.jsx
        ActivityChart.jsx
      /pages
        DashboardPage.jsx
      dashboardAPI.js

  /shared              ← Code used across multiple features
    /components        ← Reusable UI (Button, Input, Modal)
      /atoms
        Button.jsx
        Input.jsx
        Badge.jsx
      /molecules
        SearchBox.jsx
        FormField.jsx
      /organisms
        Navbar.jsx
        Sidebar.jsx
    /hooks             ← Custom hooks used everywhere
      useDebounce.js
      useLocalStorage.js
      useMediaQuery.js
    /utils             ← Pure helper functions
      formatDate.js
      validators.js
      constants.js

  /services            ← API client configuration
    apiClient.js       ← Axios instance with interceptors

  /config              ← App-wide configuration
    env.js             ← Environment variable management

  /styles              ← Global styles, themes, variables
    globals.css
    variables.css
Enter fullscreen mode Exit fullscreen mode

Why This Structure Works

Feature folder = Independent unit
       ↓
You can delete /auth and the rest of the app still works
       ↓
You can add /payments without touching anything else
       ↓
A developer can own /todo completely without conflicts
Enter fullscreen mode Exit fullscreen mode

Store Setup (Redux Toolkit)

// src/app/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "../features/auth/authSlice";
import todoReducer from "../features/todo/todoSlice";

export const store = configureStore({
  reducer: {
    auth: authReducer,
    todo: todoReducer,
  },
  // Redux DevTools enabled in dev automatically
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Enter fullscreen mode Exit fullscreen mode
// src/features/auth/authSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { loginUser } from "./authAPI";

// Async thunk for login
export const login = createAsyncThunk(
  "auth/login",
  async (credentials, thunkAPI) => {
    try {
      const response = await loginUser(credentials);
      return response.data;
    } catch (error) {
      return thunkAPI.rejectWithValue(error.response.data);
    }
  }
);

const authSlice = createSlice({
  name: "auth",
  initialState: {
    user: null,
    token: null,
    loading: false,
    error: null,
  },
  reducers: {
    logout: (state) => {
      state.user = null;
      state.token = null;
    },
    setUser: (state, action) => {
      state.user = action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(login.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(login.fulfilled, (state, action) => {
        state.loading = false;
        state.user = action.payload.user;
        state.token = action.payload.token;
      })
      .addCase(login.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});

export const { logout, setUser } = authSlice.actions;
export default authSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

4. API Layer Separation

This is one of the most important architectural decisions in a React app. Never call APIs directly from components.

The Problem with Direct API Calls

// ❌ WRONG — API call directly in component
const TodoList = () => {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    fetch("https://api.example.com/todos") // ← tightly coupled
      .then(res => res.json())
      .then(data => setTodos(data));
  }, []);
};
Enter fullscreen mode Exit fullscreen mode

Problems:

  • If the API URL changes, you search through every component
  • You can't test the component without mocking fetch globally
  • Error handling is repeated in every component
  • Auth tokens must be added everywhere manually

The Correct Pattern

Component → Custom Hook / React Query → Service Function → Axios Client → API
Enter fullscreen mode Exit fullscreen mode

Layer 1 — Axios Client (Base Config)

// src/services/apiClient.js
import axios from "axios";

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
  },
});

// Request interceptor — adds auth token to every request automatically
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor — handles 401 globally
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Redirect to login, clear token
      localStorage.removeItem("token");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

export default apiClient;
Enter fullscreen mode Exit fullscreen mode

Layer 2 — Service Functions (API operations)

// src/features/todo/todoAPI.js
import apiClient from "../../services/apiClient";

export const getTodos = (filters = {}) =>
  apiClient.get("/todos", { params: filters });

export const getTodoById = (id) =>
  apiClient.get(`/todos/${id}`);

export const createTodo = (data) =>
  apiClient.post("/todos", data);

export const updateTodo = (id, data) =>
  apiClient.put(`/todos/${id}`, data);

export const deleteTodo = (id) =>
  apiClient.delete(`/todos/${id}`);
Enter fullscreen mode Exit fullscreen mode

Layer 3 — React Query (Data fetching + caching)

// src/features/todo/hooks/useTodos.js
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getTodos, createTodo, deleteTodo } from "../todoAPI";

export const useTodos = (filters) => {
  return useQuery({
    queryKey: ["todos", filters],   // cache key — re-fetches when filters change
    queryFn: () => getTodos(filters),
    staleTime: 5 * 60 * 1000,       // data considered fresh for 5 minutes
    cacheTime: 10 * 60 * 1000,      // cache kept for 10 minutes
  });
};

export const useCreateTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: createTodo,
    onSuccess: () => {
      // Invalidate todos cache — triggers automatic re-fetch
      queryClient.invalidateQueries({ queryKey: ["todos"] });
    },
  });
};

// Layer 4 — Component (clean and simple)
const TodoList = () => {
  const { data: todos, isLoading, error } = useTodos();
  const { mutate: addTodo } = useCreateTodo();

  if (isLoading) return <Loader />;
  if (error) return <ErrorMessage message={error.message} />;

  return (
    <ul>
      {todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

Why This Layered Approach?

Benefit What It Means
Loose coupling Backend URL change → only update apiClient.js
Easy testing Mock service functions, not fetch/axios globally
Automatic caching React Query caches, deduplicates, and refetches
Centralized auth Token added/removed in one place
Reusability Same service function used in multiple hooks/components

5. Atomic Design System

Atomic Design is a methodology for building UI components that are consistent, reusable, and scalable. Inspired by chemistry — you build from the smallest pieces upward.

Atoms → Molecules → Organisms → Templates → Pages
Enter fullscreen mode Exit fullscreen mode

Level 1 — Atoms (The smallest possible UI piece)

An atom has no business logic. It is purely visual and highly reusable.

// src/shared/components/atoms/Button.jsx
export const Button = ({
  children,
  variant = "primary",   // "primary" | "secondary" | "danger"
  size = "md",           // "sm" | "md" | "lg"
  disabled = false,
  isLoading = false,
  onClick,
  ...props
}) => {
  return (
    <button
      className={`btn btn--${variant} btn--${size}`}
      disabled={disabled || isLoading}
      onClick={onClick}
      {...props}
    >
      {isLoading ? <Spinner size="sm" /> : children}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode
// src/shared/components/atoms/Input.jsx
export const Input = ({
  label,
  id,
  error,
  helperText,
  ...props
}) => {
  return (
    <div className="input-wrapper">
      {label && <label htmlFor={id}>{label}</label>}
      <input id={id} {...props} />
      {error && <span role="alert" className="input-error">{error}</span>}
      {helperText && <span className="helper-text">{helperText}</span>}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Rule for atoms: If you find yourself adding an API call or business condition, it's no longer an atom. Extract that logic out.


Level 2 — Molecules (Atoms working together)

A molecule combines 2–3 atoms to create a small, focused UI piece with light logic.

// src/shared/components/molecules/SearchBox.jsx
import { Input } from "../atoms/Input";
import { Button } from "../atoms/Button";

export const SearchBox = ({ onSearch, placeholder = "Search..." }) => {
  const [value, setValue] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (value.trim()) onSearch(value);
  };

  return (
    <form onSubmit={handleSubmit} className="search-box">
      <Input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder={placeholder}
        aria-label="Search"
      />
      <Button type="submit" variant="primary">Search</Button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode
// src/shared/components/molecules/FormField.jsx
// Combines label + input + error into one reusable form field
export const FormField = ({ label, name, error, ...inputProps }) => (
  <div className="form-field">
    <label htmlFor={name}>{label}</label>
    <Input id={name} name={name} error={error} {...inputProps} />
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Level 3 — Organisms (Complex, feature-aware sections)

Organisms can contain business logic and are feature-specific.

// src/shared/components/organisms/Navbar.jsx
import { Logo } from "../atoms/Logo";
import { SearchBox } from "../molecules/SearchBox";
import { UserMenu } from "../molecules/UserMenu";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";

export const Navbar = () => {
  const user = useSelector(state => state.auth.user);
  const navigate = useNavigate();

  const handleSearch = (query) => {
    navigate(`/search?q=${query}`);
  };

  return (
    <header className="navbar" role="banner">
      <Logo />
      <SearchBox onSearch={handleSearch} placeholder="Search todos..." />
      {user && <UserMenu user={user} />}
    </header>
  );
};
Enter fullscreen mode Exit fullscreen mode

Level 4 — Templates (Layout skeleton)

Templates define page layout without real data. They are pure structure.

// src/shared/components/templates/DashboardLayout.jsx
import { Navbar } from "../organisms/Navbar";
import { Sidebar } from "../organisms/Sidebar";

export const DashboardLayout = ({ children }) => {
  return (
    <div className="dashboard-layout">
      <Navbar />
      <div className="dashboard-body">
        <Sidebar />
        <main className="dashboard-content" role="main">
          {children}
        </main>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Level 5 — Pages (Real data + business logic)

Pages connect everything — they fetch data, handle routing, and render templates.

// src/features/dashboard/pages/DashboardPage.jsx
import { DashboardLayout } from "../../../shared/components/templates/DashboardLayout";
import { StatsCard } from "../components/StatsCard";
import { ActivityChart } from "../components/ActivityChart";
import { useDashboardStats } from "../hooks/useDashboardStats";

const DashboardPage = () => {
  const { data: stats, isLoading } = useDashboardStats();

  return (
    <DashboardLayout>
      <h1>Dashboard</h1>
      <div className="stats-grid">
        <StatsCard title="Total Todos" value={stats?.total} isLoading={isLoading} />
        <StatsCard title="Completed" value={stats?.completed} isLoading={isLoading} />
        <StatsCard title="Pending" value={stats?.pending} isLoading={isLoading} />
      </div>
      <ActivityChart data={stats?.activity} />
    </DashboardLayout>
  );
};

export default DashboardPage;
Enter fullscreen mode Exit fullscreen mode

Atomic Design vs Feature Architecture — How They Work Together

These are NOT competing approaches. Use both together:

/src
  /features
    /todo
      /components          ← Organisms specific to todo feature
        TodoList.jsx
        TodoItem.jsx
      hooks/
      todoAPI.js

  /shared
    /components
      /atoms               ← Used everywhere
        Button.jsx
        Input.jsx
      /molecules           ← Used across features
        SearchBox.jsx
      /organisms           ← Shared organisms (Navbar, Sidebar)
        Navbar.jsx
Enter fullscreen mode Exit fullscreen mode
Atomic Design Feature Architecture
Organizes UI components Organizes business features
Ensures reusability Ensures scalability
Answers "how is this built?" Answers "where does this belong?"
Used in /shared Used in /features

6. State Management Patterns

State management is where most React apps go wrong. Here are all the patterns, when to use each, and the differences between them.

Pattern 1 — Local State (useState)

Use for: UI-only state that no other component needs.

// ✅ Perfect use case for local state
const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);   // Only Modal needs this

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open</button>
      {isOpen && (
        <div role="dialog" aria-modal="true">
          <button onClick={() => setIsOpen(false)}>Close</button>
          <p>Modal content</p>
        </div>
      )}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

When NOT to use: When multiple unrelated components need the same data.


Pattern 2 — Lifting State Up

Use for: When a parent needs to coordinate between two sibling components.

// Parent owns state, passes it down
const TodoApp = () => {
  const [filter, setFilter] = useState("all");   // Both TodoList and FilterBar need this

  return (
    <>
      <FilterBar currentFilter={filter} onFilterChange={setFilter} />
      <TodoList filter={filter} />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Problem it creates: When you lift state too many levels, you get prop drilling — passing data through 5 components that don't use it, just to reach one that does.


Pattern 3 — Context API

Use for: Data that's truly "global" for a subtree — theme, language, auth.

// AuthContext.jsx
const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(() => localStorage.getItem("token"));

  const login = async (credentials) => {
    const data = await loginUser(credentials);
    setUser(data.user);
    setToken(data.token);
    localStorage.setItem("token", data.token);
  };

  const logout = () => {
    setUser(null);
    setToken(null);
    localStorage.removeItem("token");
  };

  return (
    <AuthContext.Provider value={{ user, token, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

// Custom hook for clean consumption
export const useAuth = () => {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within AuthProvider");
  return ctx;
};
Enter fullscreen mode Exit fullscreen mode

Performance warning: Context re-renders ALL consumers when any value changes. Split contexts to avoid this:

// ❌ One context causes unnecessary re-renders
<AppContext.Provider value={{ user, theme, language, todos }}>

// ✅ Split contexts — each updates independently
<AuthContext.Provider value={{ user }}>
  <ThemeContext.Provider value={{ theme }}>
    <LanguageContext.Provider value={{ language }}>
Enter fullscreen mode Exit fullscreen mode

Pattern 4 — Redux Toolkit (Global State)

Use for: Complex global state — user data, shopping cart, app-wide business logic.

// Using Redux state in a component
const UserProfile = () => {
  const user = useSelector(state => state.auth.user);    // read state
  const dispatch = useDispatch();

  const handleLogout = () => {
    dispatch(logout());     // trigger action
  };

  return (
    <div>
      <h1>Hello, {user?.name}</h1>
      <button onClick={handleLogout}>Logout</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

When NOT to use Redux for:

  • API data (use React Query instead)
  • UI state like modals (use useState)
  • Derived values like filtered lists (compute them)

Pattern 5 — React Query (Server State)

The most important pattern for 2026. API data is a completely different type of state — it needs caching, background refetching, deduplication, and loading/error states. React Query handles all of this automatically.

// Without React Query — you write all this yourself:
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
  setIsLoading(true);
  getTodos()
    .then(data => setTodos(data))
    .catch(err => setError(err))
    .finally(() => setIsLoading(false));
}, []);

// With React Query — 3 lines replace 15:
const { data: todos, isLoading, error } = useQuery({
  queryKey: ["todos"],
  queryFn: getTodos,
});
Enter fullscreen mode Exit fullscreen mode

React Query gives you for free:

  • Automatic caching (same data requested twice = 1 API call)
  • Background refetching when window regains focus
  • Loading and error states
  • Automatic retries on failure
  • Optimistic updates

Pattern 6 — Derived State (Anti-Pattern to Avoid)

Never store something that can be calculated.

// ❌ BAD — storing derived state
const [todos, setTodos] = useState([]);
const [completedCount, setCompletedCount] = useState(0);  // derived!
const [pendingTodos, setPendingTodos] = useState([]);      // derived!

// You now have 3 state variables to keep in sync. Bugs guaranteed.

// ✅ GOOD — derive from single source of truth
const [todos, setTodos] = useState([]);
const completedCount = todos.filter(t => t.done).length;   // computed
const pendingTodos = todos.filter(t => !t.done);            // computed

// One state variable. Zero sync bugs.
Enter fullscreen mode Exit fullscreen mode

Pattern 7 — Zustand (Lightweight Alternative to Redux)

Use for: Medium complexity apps where Redux feels like overkill.

// store/todoStore.js
import { create } from "zustand";
import { getTodos, addTodo } from "../features/todo/todoAPI";

export const useTodoStore = create((set, get) => ({
  todos: [],
  isLoading: false,

  fetchTodos: async () => {
    set({ isLoading: true });
    const data = await getTodos();
    set({ todos: data, isLoading: false });
  },

  addTodo: (todo) => set(state => ({
    todos: [...state.todos, todo]
  })),

  removeTodo: (id) => set(state => ({
    todos: state.todos.filter(t => t.id !== id)
  })),
}));

// In component — no Provider needed
const TodoList = () => {
  const { todos, fetchTodos } = useTodoStore();

  useEffect(() => { fetchTodos(); }, []);

  return <ul>{todos.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
};
Enter fullscreen mode Exit fullscreen mode

The Complete Decision Chart

"Where should this state live?"
              ↓
Is it only used in this component?
     YES → useState (local)
     NO  ↓
Is it needed by nearby components?
     YES → Lift up to parent
     NO  ↓
Is it API/server data?
     YES → React Query
     NO  ↓
Is it simple and app-wide (theme, language)?
     YES → Context API
     NO  ↓
Is it complex business logic (cart, auth)?
     YES → Redux Toolkit or Zustand
Enter fullscreen mode Exit fullscreen mode

7. Colocation Pattern

What Is Colocation?

"Keep state as close as possible to where it is used."

If only one component needs a piece of state, it lives inside that component. You only move it up when multiple components genuinely need it.

Why It Matters

// ❌ OVER-LIFTED STATE — the classic beginner mistake
// App.jsx owns state that only Modal needs
const App = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);  // used only in Modal!

  return (
    <>
      <Navbar isModalOpen={isModalOpen} />            {/* doesn't use it */}
      <Dashboard isModalOpen={isModalOpen} />          {/* doesn't use it */}
      <Sidebar isModalOpen={isModalOpen} />             {/* doesn't use it */}
      <Modal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
      />
    </>
  );
};
// Every time modal state changes, Navbar, Dashboard, Sidebar all re-render
// unnecessarily. Performance hit + messy code.
Enter fullscreen mode Exit fullscreen mode
// ✅ COLOCATED STATE — clean and performant
const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);  // lives here, used here

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open</button>
      {isOpen && (
        <div role="dialog">
          <button onClick={() => setIsOpen(false)}>Close</button>
          <p>Content</p>
        </div>
      )}
    </>
  );
};
// Only Modal re-renders when modal state changes. Zero prop drilling.
Enter fullscreen mode Exit fullscreen mode

The Colocation Rules

Rule 1: Start state inside the component that uses it.
Rule 2: Only lift state up when ANOTHER component needs it.
Rule 3: Only make state global when MANY components across the app need it.
Rule 4: Global state is the last resort, not the starting point.
Enter fullscreen mode Exit fullscreen mode

Real Example — Todo App State Ownership

State: todoInput text          → Inside TodoInput component (colocated)
State: filter ("all/done")     → Inside TodoList (only TodoList uses it)
State: todos array             → Lifted (TodoList + Dashboard both need it) → React Query
State: user authentication     → Global (entire app needs it) → Redux
State: app theme               → Global (entire app) → Context
Enter fullscreen mode Exit fullscreen mode

8. Accessibility (WCAG) — Deep Dive

What Is Accessibility and Why Does It Matter?

Accessibility (a11y) means building apps that everyone can use — including people who are blind, have low vision, use only a keyboard, or have cognitive disabilities.

Who uses screen readers and accessibility tools:

  • Blind users (cannot see the screen at all)
  • Low vision users (need screen magnification + audio)
  • Motor disability users (keyboard-only navigation, no mouse)
  • Elderly users
  • Temporary disabilities (broken arm, eye surgery recovery)

Why you must care:

  • 1 in 4 adults in the US has a disability
  • Many governments legally require accessible websites
  • Accessible sites rank better on Google (SEO benefit)
  • Better accessibility = better user experience for everyone

How Browsers Handle Accessibility Internally

When a browser parses your HTML, it builds three trees:

1. DOM Tree        → The actual HTML structure
2. CSSOM Tree      → Styles computed for each element
3. Accessibility Tree → What screen readers see (THIS is what matters)
Enter fullscreen mode Exit fullscreen mode

The Accessibility Tree is a simplified, semantic version of the DOM. Screen reader software (NVDA, JAWS, VoiceOver) reads this tree, not the visual screen.

<!-- HTML you write -->
<button class="btn btn-primary">Submit Form</button>

<!-- Accessibility Tree entry -->
Role: button
Name: "Submit Form"
State: enabled, focusable
Enter fullscreen mode Exit fullscreen mode

Without ARIA vs With ARIA — The Real Difference

Case 1 — Semantic HTML (No ARIA needed)

<!-- Browser automatically knows: Role=button, Name="Submit" -->
<button>Submit</button>
Enter fullscreen mode Exit fullscreen mode

Screen reader output: 🗣️ "Submit, button"

User can Tab to it, press Enter to activate it. Perfect. No ARIA needed.


Case 2 — Div used as button (BROKEN)

<!-- ❌ Browser sees: Role=generic, Name="Submit" -->
<div onclick="submit()">Submit</div>
Enter fullscreen mode Exit fullscreen mode

Screen reader output: 🗣️ "Submit" (no role announced)

Problems:

  • User doesn't know it's clickable
  • Tab key does NOT focus it
  • Enter key does NOT activate it
  • Completely broken for keyboard users

Case 3 — Div with ARIA (Fixed, but not ideal)

<!-- ✅ ARIA forces accessibility tree to show: Role=button, Name="Submit" -->
<div
  role="button"
  tabindex="0"
  onclick="submit()"
  onkeydown="if(event.key === 'Enter') submit()"
>
  Submit
</div>
Enter fullscreen mode Exit fullscreen mode

Screen reader output: 🗣️ "Submit, button"

This works, but you had to manually add tabindex, keyboard handler, and role. A native <button> gives all of this for free.

🔑 Golden Rule: Use semantic HTML first. Use ARIA only when semantic HTML is not enough.


WCAG — The 4 Principles (POUR)

1. Perceivable — Information must be visible or audible

// ✅ Alt text for images
<img src="profile.jpg" alt="Sarah's profile picture" />

// ✅ Decorative image — empty alt tells screen reader to skip it
<img src="decorative-line.png" alt="" />

// ✅ Icon buttons must have a label
<button aria-label="Delete todo item">
  <TrashIcon aria-hidden="true" />
</button>
Enter fullscreen mode Exit fullscreen mode

2. Operable — Everything must work with a keyboard

// ✅ Always use button for interactive elements (free keyboard support)
<button onClick={handleDelete}>Delete</button>

// ✅ Ensure focus is always visible
// In your CSS:
// :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; }

// ❌ NEVER DO THIS — hides focus ring without providing alternative
// button:focus { outline: none; }
Enter fullscreen mode Exit fullscreen mode

3. Understandable — UI must be clear and predictable

// ✅ Form labels connected to inputs
<div>
  <label htmlFor="email">Email Address</label>
  <input
    id="email"
    type="email"
    aria-describedby="email-error"
    aria-invalid={!!emailError}
  />
  {emailError && (
    <span id="email-error" role="alert">
      {emailError}
    </span>
  )}
</div>
Enter fullscreen mode Exit fullscreen mode

4. Robust — Works with assistive technologies

// ✅ Use semantic HTML everywhere
<header>   {/* not <div class="header"> */}
<nav>      {/* not <div class="nav"> */}
<main>     {/* not <div class="main"> */}
<article>  {/* not <div class="article"> */}
<footer>   {/* not <div class="footer"> */}
Enter fullscreen mode Exit fullscreen mode

Common ARIA Attributes Explained

// aria-label — provides an accessible name when visible text is absent
<button aria-label="Close dialog"></button>

// aria-labelledby — references another element as the label
<h2 id="modal-title">Confirm Delete</h2>
<dialog aria-labelledby="modal-title">...</dialog>

// aria-describedby — references element providing extra description
<input aria-describedby="password-hint" type="password" />
<span id="password-hint">Must be at least 8 characters</span>

// aria-expanded — state of expandable elements
<button aria-expanded={isOpen} onClick={toggle}>Menu</button>

// aria-hidden — hide decorative elements from screen readers
<span aria-hidden="true"></span>

// role — override or assign semantic role
<div role="dialog" aria-modal="true">...</div>
<ul role="listbox">...</ul>

// aria-live — announce dynamic content changes
<div aria-live="polite">
  {statusMessage}  {/* Screen reader announces changes automatically */}
</div>
Enter fullscreen mode Exit fullscreen mode

Accessible React Components — Practical Examples

Accessible Modal with focus trap:

const Modal = ({ isOpen, onClose, title, children }) => {
  const modalRef = useRef(null);
  const previousFocusRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      // Save current focus position
      previousFocusRef.current = document.activeElement;
      // Move focus into modal
      modalRef.current?.focus();
    } else {
      // Restore focus when modal closes
      previousFocusRef.current?.focus();
    }
  }, [isOpen]);

  // Close on Escape key
  const handleKeyDown = (e) => {
    if (e.key === "Escape") onClose();
  };

  if (!isOpen) return null;

  return (
    // Backdrop
    <div
      className="modal-backdrop"
      onClick={onClose}
      aria-hidden="true"
    >
      {/* Modal panel — stops click propagation */}
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        className="modal-panel"
        onClick={(e) => e.stopPropagation()}
        onKeyDown={handleKeyDown}
      >
        <h2 id="modal-title">{title}</h2>
        <div>{children}</div>
        <button onClick={onClose} aria-label="Close dialog">
          Close
        </button>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Accessible Dropdown:

const Dropdown = ({ label, options, onSelect }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedIndex, setSelectedIndex] = useState(-1);

  const handleKeyDown = (e) => {
    switch (e.key) {
      case "ArrowDown":
        e.preventDefault();
        setSelectedIndex(i => Math.min(i + 1, options.length - 1));
        break;
      case "ArrowUp":
        e.preventDefault();
        setSelectedIndex(i => Math.max(i - 1, 0));
        break;
      case "Enter":
        if (selectedIndex >= 0) {
          onSelect(options[selectedIndex]);
          setIsOpen(false);
        }
        break;
      case "Escape":
        setIsOpen(false);
        break;
    }
  };

  return (
    <div className="dropdown">
      <button
        aria-haspopup="listbox"
        aria-expanded={isOpen}
        onClick={() => setIsOpen(o => !o)}
      >
        {label}
      </button>

      {isOpen && (
        <ul
          role="listbox"
          aria-label={label}
          onKeyDown={handleKeyDown}
        >
          {options.map((option, index) => (
            <li
              key={option.value}
              role="option"
              aria-selected={index === selectedIndex}
              onClick={() => { onSelect(option); setIsOpen(false); }}
            >
              {option.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

9. Module 1: Advanced React Fundamentals


1.1 Component Design Principles

Single Responsibility Principle (SRP)

Every component should do one thing only. When a component does too many things, it becomes hard to test, reuse, and understand.

// ❌ GOD COMPONENT — does everything
const UserDashboard = () => {
  const [user, setUser] = useState(null);
  const [todos, setTodos] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [filter, setFilter] = useState("all");
  const [newTodo, setNewTodo] = useState("");
  const [isModalOpen, setIsModalOpen] = useState(false);

  useEffect(() => { /* fetch user */ }, []);
  useEffect(() => { /* fetch todos */ }, []);

  const handleAddTodo = async () => { /* ... */ };
  const handleDelete = async (id) => { /* ... */ };
  const handleFilter = () => { /* ... */ };

  return (
    <div>
      {/* 300+ lines of JSX mixing concerns */}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode
// ✅ SRP APPLIED — each component has one job
// UserDashboard orchestrates, others do the actual work

const UserDashboard = () => {
  const { data: user } = useCurrentUser();
  return (
    <DashboardLayout>
      <UserHeader user={user} />
      <TodoSection />
    </DashboardLayout>
  );
};

const UserHeader = ({ user }) => (
  <header>
    <h1>Welcome, {user?.name}</h1>
    <UserAvatar src={user?.avatar} alt={`${user?.name}'s avatar`} />
  </header>
);

const TodoSection = () => {
  const [filter, setFilter] = useState("all");
  return (
    <>
      <TodoInput />
      <TodoFilter currentFilter={filter} onFilterChange={setFilter} />
      <TodoList filter={filter} />
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Smart vs Presentational Components

This is a fundamental pattern that separates data/logic from visual rendering.

Smart (Container) Presentational (View)
Knows about Redux/API No knowledge of data source
Passes data down as props Receives everything via props
Has side effects Pure — same props = same output
Hard to reuse Highly reusable
Easy to test logic Easy to test visually (Storybook)
// ✅ SMART COMPONENT — knows about data, passes it down
const TodoListContainer = () => {
  const { data: todos, isLoading } = useTodos();
  const { mutate: deleteTodo } = useDeleteTodo();
  const [filter, setFilter] = useState("all");

  const filteredTodos = useMemo(
    () => todos?.filter(t => filter === "all" || (filter === "done") === t.done),
    [todos, filter]
  );

  return (
    <TodoListView
      todos={filteredTodos}
      isLoading={isLoading}
      filter={filter}
      onFilterChange={setFilter}
      onDelete={deleteTodo}
    />
  );
};

// ✅ PRESENTATIONAL COMPONENT — pure rendering, no business logic
const TodoListView = ({ todos, isLoading, filter, onFilterChange, onDelete }) => {
  if (isLoading) return <LoadingSpinner />;
  if (!todos?.length) return <EmptyState message="No todos yet" />;

  return (
    <section aria-label="Todo list">
      <TodoFilterBar currentFilter={filter} onChange={onFilterChange} />
      <ul>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onDelete={() => onDelete(todo.id)}
          />
        ))}
      </ul>
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

When to Extract Components

Extract when:

  • JSX is more than ~50 lines
  • The same UI appears in 2+ places
  • A section has its own distinct responsibility
  • A section has its own state that nothing else needs
  • Testing a smaller piece would be valuable

Don't extract when:

  • It would require passing many props to a component used only once
  • The "component" has no reuse value
  • Extraction makes the code harder to follow (over-engineering)

Composition Patterns

Children prop — The most underused pattern:

// ✅ Flexible container using children
const Card = ({ title, children, footer }) => (
  <div className="card">
    <div className="card-header"><h3>{title}</h3></div>
    <div className="card-body">{children}</div>
    {footer && <div className="card-footer">{footer}</div>}
  </div>
);

// Usage — Card doesn't need to know what's inside it
<Card
  title="User Profile"
  footer={<button>Edit</button>}
>
  <UserAvatar src={user.avatar} />
  <p>{user.bio}</p>
</Card>
Enter fullscreen mode Exit fullscreen mode

Render Props — For sharing logic with custom rendering:

// ✅ DataFetcher renders nothing itself — caller decides the UI
const DataFetcher = ({ url, render }) => {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetch(url).then(r => r.json()).then(d => {
      setData(d);
      setIsLoading(false);
    });
  }, [url]);

  return render({ data, isLoading });
};

// Usage
<DataFetcher
  url="/api/user"
  render={({ data: user, isLoading }) =>
    isLoading ? <Spinner /> : <UserCard user={user} />
  }
/>
Enter fullscreen mode Exit fullscreen mode

1.2 State Management Mastery

Controlled vs Uncontrolled Inputs

// CONTROLLED — React owns the value
const ControlledInput = () => {
  const [value, setValue] = useState("");

  return (
    <input
      value={value}                            // React controls the value
      onChange={(e) => setValue(e.target.value)}
    />
  );
};
// When to use: Most forms. You can validate on every keystroke,
// transform input, and React state always reflects the UI value.

// UNCONTROLLED — DOM owns the value (accessed via ref)
const UncontrolledInput = () => {
  const inputRef = useRef(null);

  const handleSubmit = () => {
    console.log(inputRef.current.value);       // read value only on submit
  };

  return (
    <>
      <input ref={inputRef} defaultValue="" />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
};
// When to use: File inputs (must be uncontrolled), integration with
// non-React libraries, or when you truly only need value on submit.
Enter fullscreen mode Exit fullscreen mode

State Normalization — Avoiding Nested Mutation Bugs

// ❌ NESTED STATE — mutation bugs waiting to happen
const [todos, setTodos] = useState([
  {
    id: 1,
    title: "Buy groceries",
    comments: [
      { id: 1, text: "Get milk" },
      { id: 2, text: "Get bread" },
    ]
  }
]);

// Updating a comment requires deep cloning — easy to get wrong
const updateComment = (todoId, commentId, newText) => {
  setTodos(todos.map(todo =>
    todo.id === todoId
      ? {
          ...todo,
          comments: todo.comments.map(c =>
            c.id === commentId ? { ...c, text: newText } : c
          )
        }
      : todo
  ));
};

// ✅ NORMALIZED STATE — flat structure, easy updates
const [todosById, setTodosById] = useState({
  1: { id: 1, title: "Buy groceries", commentIds: [1, 2] }
});
const [commentsById, setCommentsById] = useState({
  1: { id: 1, text: "Get milk" },
  2: { id: 2, text: "Get bread" },
});

// Updating a comment is simple — no deep cloning
const updateComment = (commentId, newText) => {
  setCommentsById(prev => ({
    ...prev,
    [commentId]: { ...prev[commentId], text: newText }
  }));
};
Enter fullscreen mode Exit fullscreen mode

Handling Async State Consistently

Never scatter isLoading, error, and data across independent useState calls. Group them properly or use a reducer.

// ❌ SCATTERED ASYNC STATE — multiple useState can get out of sync
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Easy to have isLoading=true and data=something simultaneously

// ✅ OPTION 1 — useReducer for consistent state transitions
const asyncReducer = (state, action) => {
  switch (action.type) {
    case "LOADING":  return { status: "loading",  data: null,          error: null };
    case "SUCCESS":  return { status: "success",  data: action.payload, error: null };
    case "ERROR":    return { status: "error",    data: null,          error: action.error };
    default:         return state;
  }
};

const [state, dispatch] = useReducer(asyncReducer, {
  status: "idle",
  data: null,
  error: null
});

// ✅ OPTION 2 — Custom hook that encapsulates the pattern
const useAsync = (asyncFn, deps = []) => {
  const [state, dispatch] = useReducer(asyncReducer, {
    status: "idle", data: null, error: null
  });

  useEffect(() => {
    dispatch({ type: "LOADING" });
    asyncFn()
      .then(data => dispatch({ type: "SUCCESS", payload: data }))
      .catch(error => dispatch({ type: "ERROR", error }));
  }, deps);

  return state;
};

// Usage
const { status, data, error } = useAsync(() => getTodos());
Enter fullscreen mode Exit fullscreen mode

1.3 Hooks at a Professional Level

useEffect — The Real Mental Model

useEffect is for synchronizing with external systems — not for running code after render.

External systems include: APIs, timers, WebSockets, browser APIs, third-party libraries.

// ✅ CORRECT use cases for useEffect
// 1. Syncing with external API
useEffect(() => {
  const subscription = websocket.subscribe("/chat", setMessages);
  return () => subscription.unsubscribe();   // cleanup when component unmounts
}, []);

// 2. Syncing document title with state
useEffect(() => {
  document.title = `${unreadCount} unread messages`;
}, [unreadCount]);

// 3. Setting up a timer
useEffect(() => {
  const interval = setInterval(() => {
    setTime(new Date());
  }, 1000);
  return () => clearInterval(interval);  // cleanup to prevent memory leak
}, []);
Enter fullscreen mode Exit fullscreen mode
// ❌ WRONG — using useEffect for derived computation
useEffect(() => {
  setFilteredTodos(todos.filter(t => t.done));  // NO! Compute during render
}, [todos]);

// ✅ CORRECT — derive during render (no useEffect needed)
const filteredTodos = todos.filter(t => t.done);

// ❌ WRONG — using useEffect to update parent
useEffect(() => {
  onTodosChange(todos);  // This causes re-render chains
}, [todos]);

// ✅ CORRECT — pass callback to parent, parent calls it when needed
const handleToggle = (id) => {
  const updated = toggleTodo(id);
  onTodosChange(updated);
};
Enter fullscreen mode Exit fullscreen mode

Dependency Array Mastery — Avoiding Stale Closures

// ❌ STALE CLOSURE — count in callback is always 0
const Counter = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      console.log(count);     // Always logs 0 — stale closure!
      setCount(count + 1);    // Always sets to 1
    }, 1000);
    return () => clearInterval(interval);
  }, []);   // Empty deps — callback captures count=0 forever
};

// ✅ FIX 1 — Add count to dependency array
useEffect(() => {
  const interval = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(interval);
}, [count]);   // Re-creates interval when count changes

// ✅ FIX 2 (Better) — Use functional update form
useEffect(() => {
  const interval = setInterval(() => {
    setCount(prev => prev + 1);   // Gets current value, no dependency needed
  }, 1000);
  return () => clearInterval(interval);
}, []);   // Empty deps, no stale closure
Enter fullscreen mode Exit fullscreen mode

useRef — The Right Uses

// Use 1 — Accessing DOM elements
const FocusInput = () => {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </>
  );
};

// Use 2 — Storing mutable values WITHOUT causing re-renders
const Timer = () => {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);   // storing timer ID — we don't want re-render when this changes

  const start = () => {
    intervalRef.current = setInterval(() => setTime(t => t + 1), 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <div>
      <span>{time}s</span>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
};

// Use 3 — Previous value tracking (no extra state, no extra re-render)
const usePreviousValue = (value) => {
  const prevRef = useRef(undefined);

  useEffect(() => {
    prevRef.current = value;
  });  // No dependency array — runs after every render

  return prevRef.current;
};

// Usage
const prev = usePreviousValue(count);
Enter fullscreen mode Exit fullscreen mode

Custom Hooks — Design Principles

A custom hook extracts reusable logic. Good hooks have:

  • A clear, verb-first name (useXxx)
  • A single responsibility
  • Predictable inputs and outputs
  • Proper cleanup
// ✅ Well-designed custom hook — single responsibility, clean API
const useDebounce = (value, delay = 300) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);  // Clean up on value change
  }, [value, delay]);

  return debouncedValue;
};

// Usage
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearch = useDebounce(searchTerm, 500);

useEffect(() => {
  if (debouncedSearch) searchAPI(debouncedSearch);
}, [debouncedSearch]);
Enter fullscreen mode Exit fullscreen mode
// ✅ Custom hook that encapsulates complex logic
const useLocalStorage = (key, initialValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function
        ? value(storedValue)
        : value;
      setStoredValue(valueToStore);
      localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
};

// Usage — works exactly like useState but persists across refreshes
const [theme, setTheme] = useLocalStorage("theme", "light");
Enter fullscreen mode Exit fullscreen mode

Common Hook Mistakes

// ❌ MISTAKE 1 — Conditional hooks (NEVER do this — breaks Rules of Hooks)
const Component = ({ isLoggedIn }) => {
  if (isLoggedIn) {
    const [data, setData] = useState(null);  // Hook inside condition!
  }
};

// ✅ Fix — always call hooks unconditionally, use condition inside
const Component = ({ isLoggedIn }) => {
  const [data, setData] = useState(null);
  // Use isLoggedIn in logic, not around the hook
};

// ❌ MISTAKE 2 — Creating new objects/functions in render as dependencies
const Component = () => {
  const config = { timeout: 5000 };   // New object every render!

  useEffect(() => {
    setup(config);
  }, [config]);   // This runs on EVERY render (config is always "new")
};

// ✅ Fix — move stable values outside component or use useMemo/useCallback
const config = { timeout: 5000 };   // Outside component — never changes

// ❌ MISTAKE 3 — Missing cleanup causing memory leaks
useEffect(() => {
  let timer = setTimeout(() => setIsVisible(true), 2000);
  // If component unmounts before 2s, timer still fires → state update on unmounted component
});

// ✅ Fix — always clean up
useEffect(() => {
  let timer = setTimeout(() => setIsVisible(true), 2000);
  return () => clearTimeout(timer);   // cleanup
}, []);
Enter fullscreen mode Exit fullscreen mode

10. Full Architecture Summary + Interview Answers

The Complete Mental Model

User visits the app
       ↓
React Router → determines which Page to render
       ↓
Page component → connects to data sources
   - useQuery() for server data
   - useSelector() for Redux global state
   - useState() for local UI state
       ↓
Page renders Template (layout structure)
       ↓
Template renders Organisms (Navbar, Sidebar)
       ↓
Organisms render Molecules (SearchBox, FormField)
       ↓
Molecules render Atoms (Button, Input, Badge)
       ↓
User sees the UI
       ↓
User interacts → event handler triggered
       ↓
dispatch(action) OR mutate() OR setState()
       ↓
State updates → React re-renders minimal components
       ↓
UI updates
Enter fullscreen mode Exit fullscreen mode

Quick Reference Decision Guide

FOLDER STRUCTURE
  Feature code?  → /features/{name}/
  Shared UI?     → /shared/components/{level}/
  API calls?     → /services/ + /features/{name}/api.js

STATE MANAGEMENT
  Only one component?     → useState (local)
  Nearby components?      → Lift up to parent
  Server/API data?        → React Query
  Lightweight global?     → Context API or Zustand
  Complex business logic? → Redux Toolkit

COMPONENT TYPE
  No logic, just render?       → Presentational (atoms/molecules)
  Has data-fetching?           → Smart (container)
  Complex reusable section?    → Organism
  Layout skeleton?             → Template
  Full page with data?         → Page

ACCESSIBILITY
  Interactive element?   → Use native HTML button/a/input
  Dynamic content?       → aria-live
  Icon-only button?      → aria-label
  Modal?                 → role="dialog" + focus management
  Form field?            → label + id + aria-describedby
Enter fullscreen mode Exit fullscreen mode

Interview-Ready Answers

Q: What is scalable frontend architecture?

Scalable frontend architecture means designing a React app with feature-based folder structure, centralized but selective state management, abstracted API layers, lazy loading for performance, and proper testing — so the app can grow in users, features, and team size without becoming unmaintainable.

Q: When do you use Redux vs Context vs React Query?

Redux Toolkit for complex global business state (auth, cart). Context API for simple app-wide values (theme, language). React Query for all server/API data — it handles caching, loading states, and background refetching automatically. I avoid storing API data in Redux.

Q: What is Atomic Design and how do you apply it?

Atomic Design organizes UI into atoms (smallest pieces like Button), molecules (atoms combined like SearchBox), organisms (complex sections like Navbar), templates (page layouts), and pages (real data + business logic). I combine it with feature-based architecture — atoms and molecules live in /shared/components, feature-specific organisms live inside their feature folder.

Q: What is the Colocation Pattern?

Colocation means keeping state as close as possible to where it's used. I start with state local to the component, only lift it to a parent when another component needs it, and only make it global when it's truly needed app-wide. This reduces unnecessary re-renders and keeps code easy to understand.

Q: What is accessibility and how do you implement it in React?

Accessibility ensures the app is usable by everyone, including those using screen readers, keyboard-only navigation, or assistive technologies. I use semantic HTML as the first approach, ARIA attributes when semantics aren't sufficient, always connect form labels to inputs, ensure keyboard focus is visible, manage focus in modals, and use tools like Lighthouse to audit my work.

Q: What is the correct mental model for useEffect?

useEffect is for synchronizing React with external systems — APIs, timers, WebSockets, browser APIs. It's not for running code after render or for computing derived values. I always provide cleanup functions for subscriptions and timers, and I use functional updates in setters to avoid stale closures when the dependency array would otherwise be complex.


Found this useful? Drop a ❤️ and share it. Questions or additions? Comments are open — I read every one.

Next in this series: Performance optimization (memoization, virtualization, bundle analysis) + Testing strategies (Jest + Cypress + Testing Library).

react #javascript #webdev #frontend #architecture #systemdesign #accessibility #beginners #intermediate

Top comments (0)