Component architecture, state management trade-offs, rendering strategies (CSR/SSR/SSG/ISR), and the patterns senior engineers use to keep large codebases maintainable.
Component architecture, state management trade-offs, rendering strategies (CSR/SSR/SSG/ISR), and the patterns senior engineers use to keep large codebases maintainable.
Lesson outline
React is a library, not a framework — it solves the view layer. Everything else (routing, state, data fetching, caching, forms, auth) you have to solve yourself or pick a library for. A junior React developer picks libraries based on popularity. A senior developer picks based on the specific problem being solved.
Most React codebases collapse under their own weight after 6-12 months because they lack architectural decisions: where does state live? What owns data fetching? How do components communicate? How do we keep re-renders fast? Answering these questions upfront is frontend architecture.
The most durable React applications separate concerns into layers:
UI Components (dumb/presentational): Only props, no business logic, no data fetching. Easy to test, easy to reuse, easy to swap implementations. `<Button>`, `<Card>`, `<DataTable>` — your design system lives here.
Container/Feature Components: Compose UI components, may call hooks, may dispatch actions. Owns the "widget" logic. `<OrderSummary>` — knows what to display and when, but does not know how to fetch the order.
Pages: Route-level components. Orchestrate feature components. May fetch top-level data via SSR or useQuery.
Hooks: Extract reusable stateful logic. `useOrderStatus`, `useInfiniteScroll`, `useDebounce`. A hook is a pure function that returns state and callbacks — pure gold for testability.
The "smart/dumb" component split is often wrong
Strict smart/dumb separation breaks down when data-fetching libraries (React Query, SWR) put caching at the component level. A better mental model: UI layer (renders), hook layer (logic + data), service layer (API calls). Keep each layer single-purpose.
1// ── Layer 1: Pure UI Component (no state, no fetching) ─────────────────Pure UI component: easy to test in Storybook, zero dependencies2interface OrderCardProps {3orderId: string;4status: 'pending' | 'shipped' | 'delivered';5total: number;6onCancel: (id: string) => void;7}89export function OrderCard({ orderId, status, total, onCancel }: OrderCardProps) {10return (11<div className="border rounded-lg p-4">12<span className="text-sm text-gray-500">#{orderId}</span>13<StatusBadge status={status} />14<p className="font-bold">${total.toFixed(2)}</p>15{status === 'pending' && (16<button onClick={() => onCancel(orderId)}>Cancel</button>17)}18</div>19);20}2122// ── Layer 2: Hook (data + logic) ─────────────────────────────────────────React Query handles caching, background refetch, loading states — do not reinvent this23function useOrders(userId: string) {24const { data, isLoading, error } = useQuery({25queryKey: ['orders', userId],26queryFn: () => api.getOrders(userId),27staleTime: 30_000,28});2930const cancelOrder = useMutation({31mutationFn: (orderId: string) => api.cancelOrder(orderId),32onSuccess: () => queryClient.invalidateQueries({ queryKey: ['orders', userId] }),33});3435return {36orders: data ?? [],37isLoading,38error,39cancelOrder: cancelOrder.mutate,40};41}4243// ── Layer 3: Feature Component (composes hook + UI) ───────────────────────Feature component is thin — just wires hook to UI44export function OrderList({ userId }: { userId: string }) {45const { orders, isLoading, cancelOrder } = useOrders(userId);4647if (isLoading) return <OrderListSkeleton />;4849return (50<div className="space-y-4">51{orders.map(order => (52<OrderCard53key={order.id}54{...order}55onCancel={cancelOrder}56/>57))}58</div>59);60}
State has two fundamentally different categories with different solutions:
Server state (data from your API): Async, potentially stale, cached. Use TanStack Query (React Query) or SWR. They handle caching, background refetch, optimistic updates, deduplication of concurrent requests, and loading/error states. Do not put server state in Zustand or Redux.
Client state (UI state): Synchronous, local to the browser. Sub-categories:
When to reach for Zustand/Redux: Only when you have complex, global, synchronous client state that needs to be shared across many unrelated components. This is rarer than most developers think. A "user session" context and React Query covers 80% of apps.
Redux is often over-engineering
If you are using Redux to store API response data, you are duplicating React Query's job and creating sync bugs. Redux is powerful for event-sourced client state (undo/redo, collaborative editing) — not for caching API data.
1// State management decision framework23// 1. IS IT SERVER DATA (from an API)?4// → Use React Query / SWR. Do NOT put in global store.React Query caches this automatically — no Redux needed5const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });6const { data: orders } = useQuery({ queryKey: ['orders', userId], queryFn: fetchOrders });78// 2. IS IT URL STATE (current page/filters)?9// → Use URL search params. Shareable, bookmarkable, free.10const [searchParams, setSearchParams] = useSearchParams();11const role = searchParams.get('role') ?? 'all';URL state is free persistence and shareability — use it12const page = parseInt(searchParams.get('page') ?? '1');1314// 3. IS IT LOCAL UI STATE (open/closed, form values)?15// → useState or useReducer in the component.16const [isModalOpen, setIsModalOpen] = useState(false);1718// 4. IS IT SHARED UI STATE (theme, auth, notifications)?19// → React Context for simple cases, Zustand for complex.20const { theme, setTheme } = useThemeContext();2122// Zustand store — only for genuinely shared client state23interface UIStore {24sidebarCollapsed: boolean;25notificationQueue: Notification[];26toggleSidebar: () => void;27addNotification: (n: Notification) => void;28}2930const useUIStore = create<UIStore>((set) => ({31sidebarCollapsed: false,32notificationQueue: [],33toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),34addNotification: (n) => set((s) => ({35notificationQueue: [...s.notificationQueue, n]36})),37}));
CSR (Client-Side Rendering): The server sends an empty HTML shell + JS bundle. React renders everything in the browser. Best for authenticated apps (dashboards, SaaS) where SEO does not matter and personalization is high. Downside: slow Time To Interactive (TBI) on slow devices.
SSR (Server-Side Rendering): Server runs React, sends fully-rendered HTML. Great for SEO, fast First Contentful Paint (FCP). But server must render every request — higher server CPU cost, harder caching.
SSG (Static Site Generation): HTML is generated at build time. Fastest possible delivery via CDN. Best for marketing pages, docs, blogs. Content can only change with a rebuild.
ISR (Incremental Static Regeneration): SSG + automatic background revalidation. Pages are served as static HTML but revalidated after a configurable interval. Best of both worlds for semi-dynamic content (product pages, news articles).
| Strategy | Initial Load | SEO | Personalization | Server Cost | Best For |
|---|---|---|---|---|---|
| CSR | Slow (JS + hydrate) | Poor | Excellent | Low | Dashboards, authenticated apps |
| SSR | Fast (server renders) | Excellent | Good | High | E-commerce, social media |
| SSG | Fastest (CDN) | Excellent | None | Minimal | Marketing, docs, blogs |
| ISR | Fast (CDN + revalidate) | Excellent | Limited | Low | Product pages, news articles |
React re-renders a component and all its children whenever state or props change. In large trees, unnecessary re-renders kill performance. The tools:
React.memo: Prevent re-render of a component if its props have not changed (shallow comparison). Use on expensive components that receive the same props frequently.
useMemo: Memoize a computed value. Only recompute when dependencies change. Use for expensive computations (sorting 10,000 items, complex derived data). Avoid for cheap computations — the memoization overhead is not free.
useCallback: Memoize a function reference. Important when passing callbacks to `React.memo` components — without it, the callback reference changes every render, defeating memo.
Virtualization: Only render visible rows in long lists (react-window, react-virtual). A list of 10,000 items renders only 20-30 at a time. Essential for data tables and feeds.
Code splitting: `React.lazy` + `Suspense` to split the bundle. The initial bundle loads only the current page's code. Other pages are loaded on demand.
Profile before optimizing
Premature memoization is a performance anti-pattern. useMemo and useCallback have overhead. Use React DevTools Profiler to identify actual bottlenecks before adding memoization. Fixing a wrong problem makes your code worse, not better.
1import { memo, useMemo, useCallback } from 'react';2import { FixedSizeList as List } from 'react-window';34// ✅ Code splitting — only loads when /dashboard route is visited5const Dashboard = lazy(() => import('./pages/Dashboard'));67// ✅ React.memo — only re-renders when order prop changes8const OrderRow = memo(function OrderRow({memo is useless without useCallback on the onCancel prop9order,10onCancel11}: {12order: Order;13onCancel: (id: string) => void14}) {15return <div>{order.id}: {order.total}</div>;16});1718function OrdersPage({ userId }: { userId: string }) {19const { data: orders = [] } = useQuery({ queryKey: ['orders', userId], queryFn: fetchOrders });2021// ✅ useMemo — expensive sort only recomputed when orders change22const sortedOrders = useMemo(23() => [...orders].sort((a, b) => b.createdAt - a.createdAt),24[orders]25);26useCallback without memo on OrderRow is pointless overhead27// ✅ useCallback — stable reference so OrderRow.memo works28const handleCancel = useCallback(29(orderId: string) => cancelOrder(orderId),30[] // no dependencies — cancelOrder is stable31);3233// ✅ Virtualized list — renders only visible rowsreact-window renders only ~8 rows instead of 10,00034return (35<List36height={600}37itemCount={sortedOrders.length}38itemSize={72}39width="100%"40>41{({ index, style }) => (42<div style={style}>43<OrderRow order={sortedOrders[index]} onCancel={handleCancel} />44</div>45)}46</List>47);48}
JavaScript errors in React component trees crash the entire UI unless caught by an Error Boundary. An Error Boundary is a class component that implements `componentDidCatch` and renders a fallback UI when a child throws.
Strategy: place Error Boundaries at route level (prevent one page from crashing others), around "risky" widgets (data-dependent UIs that might fail), and around dynamic imports (code-split components that might fail to load).
React Suspense: The sibling of Error Boundaries for async states. Wrap async components with `<Suspense fallback={<Spinner />}>` — while the component is loading (data fetch, lazy import), the fallback renders. Combined with Error Boundary = resilient async UIs.
1import { Component, Suspense, lazy } from 'react';23// Error Boundary (class component — React requires this)4class ErrorBoundary extends Component<5{ fallback: ReactNode; onError?: (error: Error) => void; children: ReactNode },6{ hasError: boolean; error: Error | null }7> {8state = { hasError: false, error: null };910static getDerivedStateFromError(error: Error) {11return { hasError: true, error };12}1314componentDidCatch(error: Error, info: ErrorInfo) {componentDidCatch is the right place to report to Sentry/Datadog15// Report to Sentry, Datadog, etc.16this.props.onError?.(error);17reportError(error, { componentStack: info.componentStack });18}1920render() {21if (this.state.hasError) return this.props.fallback;22return this.props.children;23}24}2526// Usage: Error Boundary + Suspense for resilient lazy-loaded features27const Analytics = lazy(() => import('./Analytics'));2829function Dashboard() {30return (ErrorBoundary wraps Suspense — catches both render errors and failed lazy imports31<ErrorBoundary32fallback={<div>Analytics failed to load. <button>Retry</button></div>}33onError={(e) => console.error('Dashboard error:', e)}34>35<Suspense fallback={<AnalyticsSkeleton />}>36<Analytics />37</Suspense>38</ErrorBoundary>39);40}
Frontend architecture interviews test whether you can make technology choices and defend them, not just implement features.
Common questions:
Strong answers include:
Red flags:
Quick check · Frontend Architecture & State Management
1 / 1
Key takeaways
From the books
Fluent React — Tejas Kumar (2024)
Chapter 8: Performance Optimization
React's rendering model is a tree diffing algorithm. Understanding what causes re-renders (reference equality, context updates, parent renders) makes you 10x better at performance optimization than blindly adding memo everywhere.
Ready to see how this works in the cloud?
Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.
View role-based pathsSign in to track your progress and mark lessons complete.
Questions? Discuss in the community or start a thread below.
Join DiscordSign in to start or join a thread.