Back to Articles
40 min read

Advanced React Ecosystem: Performance, Concurrent Features, and Testing at Scale

Moving beyond syntax, this guide addresses the 'Day 2' challenges of React engineering. We explore deep performance optimization (virtualization, tree shaking), the architectural paradigm shift of React Server Components and Concurrent Rendering, and establish a rigorous testing pyramid using Jest, React Testing Library, and Cypress.

Performance Optimization

React.memo

React.memo is a higher-order component that memoizes functional components, preventing re-renders when props haven't changed—use it for components that render often with the same props.

const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) { // Only re-renders if `data` changes (shallow comparison) return <div>{/* complex rendering */}</div>; }); // With custom comparison const MemoizedList = React.memo(TodoList, (prevProps, nextProps) => { return prevProps.items.length === nextProps.items.length; });
Without memo:              With memo:
Parent renders             Parent renders
     ↓                          ↓
Child ALWAYS renders       Props changed?
                               ↓     ↓
                              Yes    No
                               ↓      ↓
                            Render   Skip!

useMemo Hook

useMemo memoizes expensive computed values, recalculating only when dependencies change—preventing costly recalculations on every render.

function ProductList({ products, filter }) { // Only recalculates when products or filter changes const filteredProducts = useMemo(() => { console.log('Filtering...'); // Won't run on every render return products.filter(p => p.category === filter); }, [products, filter]); const stats = useMemo(() => ({ total: filteredProducts.length, avgPrice: filteredProducts.reduce((a, b) => a + b.price, 0) / filteredProducts.length, }), [filteredProducts]); return <Grid items={filteredProducts} stats={stats} />; }

useCallback Hook

useCallback memoizes function references, preventing child components wrapped in React.memo from re-rendering due to new function instances being created on each parent render.

function Parent() { const [count, setCount] = useState(0); // Without useCallback: new function every render // With useCallback: same function reference const handleClick = useCallback(() => { console.log('Clicked!'); }, []); // Empty deps = never recreated const handleUpdate = useCallback((id) => { setItems(prev => prev.map(item => item.id === id ? { ...item, updated: true } : item )); }, []); // Stable reference using functional update return <MemoizedChild onClick={handleClick} />; }

Code Splitting

Code splitting breaks your bundle into smaller chunks loaded on demand, reducing initial load time by only downloading code when it's actually needed.

Without Code Splitting:          With Code Splitting:
┌────────────────────┐          ┌──────────┐
│                    │          │  Main    │ ← Initial load
│   Single Large     │          │  Bundle  │
│     Bundle         │          └────┬─────┘
│     (2MB)          │               │
│                    │          ┌────┴─────┬──────────┐
└────────────────────┘          │          │          │
                              Chunk A   Chunk B   Chunk C
                              (200KB)   (300KB)   (150KB)
                                 ↑
                           Loaded on demand

React.lazy

React.lazy enables dynamic importing of components, automatically code-splitting them into separate bundles that load only when the component is first rendered.

import React, { Suspense, lazy } from 'react'; // Instead of: import HeavyChart from './HeavyChart'; const HeavyChart = lazy(() => import('./HeavyChart')); const AdminPanel = lazy(() => import('./AdminPanel')); function Dashboard() { return ( <Suspense fallback={<ChartSkeleton />}> <HeavyChart data={data} /> </Suspense> ); }

Suspense

Suspense is a React component that displays fallback content while waiting for lazy-loaded components or data to load, providing a declarative way to handle loading states.

import { Suspense, lazy } from 'react'; const Comments = lazy(() => import('./Comments')); const Photos = lazy(() => import('./Photos')); function Post() { return ( <article> <PostContent /> <Suspense fallback={<CommentsSkeleton />}> <Comments /> </Suspense> <Suspense fallback={<PhotosGrid placeholder />}> <Photos /> </Suspense> </article> ); }
┌─────────────────────────────────┐
│         <Suspense>              │
│  ┌───────────────────────────┐  │
│  │ Child loading? → Fallback │  │
│  │ Child ready?   → Content  │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘

Dynamic Imports

Dynamic imports use JavaScript's import() syntax to load modules asynchronously at runtime, enabling code splitting for any module—not just React components.

// Dynamic component import const Chart = lazy(() => import('./Chart')); // Dynamic utility import async function handleExport() { const { exportToPDF } = await import('./utils/export'); exportToPDF(data); } // Dynamic import with named export const Modal = lazy(() => import('./components').then(module => ({ default: module.Modal })) ); // Prefetching const prefetchDashboard = () => import('./Dashboard'); onMouseEnter={prefetchDashboard} // Load on hover

Route-Based Splitting

Route-based code splitting loads page components only when users navigate to specific routes, naturally aligning code splitting with user navigation patterns.

import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { Suspense, lazy } from 'react'; const Home = lazy(() => import('./pages/Home')); const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); function App() { return ( <BrowserRouter> <Suspense fallback={<PageLoader />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> </BrowserRouter> ); }

Component-Based Splitting

Component-based splitting lazily loads heavy components within a page based on user interaction or viewport visibility, optimizing initial page load while deferring non-critical UI.

const HeavyEditor = lazy(() => import('./HeavyEditor')); const AdvancedFilters = lazy(() => import('./AdvancedFilters')); function ProductPage() { const [showFilters, setShowFilters] = useState(false); const [isEditing, setIsEditing] = useState(false); return ( <div> <ProductInfo /> <button onClick={() => setShowFilters(true)}> Show Advanced Filters </button> {showFilters && ( <Suspense fallback={<FiltersSkeleton />}> <AdvancedFilters /> </Suspense> )} {isEditing && ( <Suspense fallback={<EditorLoading />}> <HeavyEditor /> </Suspense> )} </div> ); }

Virtualization

Virtualization renders only visible items in large lists/grids, dramatically improving performance by reducing DOM nodes from thousands to just dozens regardless of data size.

Without Virtualization:          With Virtualization:
┌────────────────────┐          ┌────────────────────┐
│ Item 1   (in DOM)  │          │                    │ ← Buffer
│ Item 2   (in DOM)  │          ├────────────────────┤
│ Item 3   (in DOM)  │          │ Item 45  (in DOM)  │ ↑
│ ...                │          │ Item 46  (in DOM)  │ │ Visible
│ Item 9998 (in DOM) │          │ Item 47  (in DOM)  │ │ Viewport
│ Item 9999 (in DOM) │          │ Item 48  (in DOM)  │ ↓
│ Item 10000(in DOM) │          ├────────────────────┤
└────────────────────┘          │                    │ ← Buffer
   10000 DOM nodes              └────────────────────┘
                                   ~10 DOM nodes!

react-window

react-window is a lightweight virtualization library (part of the react-window family by Brian Vaughn) that efficiently renders large lists and grids by only mounting visible items plus a small overscan buffer.

import { FixedSizeList } from 'react-window'; function VirtualList({ items }) { const Row = ({ index, style }) => ( <div style={style} className="row"> {items[index].name} </div> ); return ( <FixedSizeList height={400} width={300} itemCount={items.length} itemSize={35} // Row height in pixels > {Row} </FixedSizeList> ); }

react-virtualized

react-virtualized is a comprehensive virtualization library offering more features than react-window—including auto-sizers, infinite loaders, multi-grids, and cell measurers—at the cost of larger bundle size.

import { List, AutoSizer } from 'react-virtualized'; function VirtualizedList({ items }) { const rowRenderer = ({ index, key, style }) => ( <div key={key} style={style}> {items[index].name} </div> ); return ( <AutoSizer> {({ height, width }) => ( <List height={height} width={width} rowCount={items.length} rowHeight={50} rowRenderer={rowRenderer} /> )} </AutoSizer> ); }

Debouncing and Throttling

Debouncing delays execution until input stops, while throttling limits execution to once per interval—both essential for optimizing search inputs, scroll handlers, and resize events.

import { useMemo, useState, useEffect } from 'react'; import debounce from 'lodash/debounce'; function SearchInput({ onSearch }) { const [value, setValue] = useState(''); // Debounce: wait 300ms after user stops typing const debouncedSearch = useMemo( () => debounce((query) => onSearch(query), 300), [onSearch] ); useEffect(() => { return () => debouncedSearch.cancel(); // Cleanup }, [debouncedSearch]); return ( <input value={value} onChange={(e) => { setValue(e.target.value); debouncedSearch(e.target.value); }} /> ); }
Debounce (300ms):
Keystrokes: H─e─l─l─o─────────────────
                        ↓
API Call:              "Hello"

Throttle (300ms):
Scroll Events: ││││││││││││││││││││││
Handler Calls: │     │     │     │
              0ms  300ms 600ms 900ms

Web Workers with React

Web Workers run JavaScript in background threads, keeping the main UI thread responsive during CPU-intensive operations like data processing, complex calculations, or image manipulation.

// worker.js self.onmessage = (e) => { const result = heavyComputation(e.data); self.postMessage(result); }; // Component function DataProcessor() { const [result, setResult] = useState(null); const workerRef = useRef(null); useEffect(() => { workerRef.current = new Worker(new URL('./worker.js', import.meta.url)); workerRef.current.onmessage = (e) => setResult(e.data); return () => workerRef.current.terminate(); }, []); const process = (data) => { workerRef.current.postMessage(data); }; return <button onClick={() => process(largeData)}>Process</button>; }

useTransition for Non-Urgent Updates

useTransition marks state updates as non-urgent, allowing React to interrupt them to keep the UI responsive—perfect for filtering large lists or navigating between tabs without blocking user input.

import { useState, useTransition } from 'react'; function FilteredList({ items }) { const [filter, setFilter] = useState(''); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; // Urgent: update input immediately setFilter(value); // Non-urgent: filter list can wait startTransition(() => { setFilteredItems(items.filter(item => item.name.includes(value) )); }); }; return ( <> <input value={filter} onChange={handleChange} /> {isPending && <Spinner />} <List items={filteredItems} /> </> ); }

useDeferredValue

useDeferredValue defers updating a value until more urgent updates complete, essentially creating a "lagged" version of a value that won't block high-priority renders.

import { useState, useDeferredValue, memo } from 'react'; function Search() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; return ( <> <input value={query} onChange={e => setQuery(e.target.value)} /> <div style={{ opacity: isStale ? 0.5 : 1 }}> <SearchResults query={deferredQuery} /> </div> </> ); } const SearchResults = memo(function SearchResults({ query }) { // Expensive render with deferred value const results = searchItems(query); // Won't block typing return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>; });

Concurrent Features

React's concurrent features allow rendering to be interrupted, enabling the UI to remain responsive during expensive updates by prioritizing urgent interactions over background work.

Traditional Rendering:
┌────────────────────────────────────┐
│████████████████████████████████████│ ← Blocks until complete
└────────────────────────────────────┘
User input blocked ────────────────→

Concurrent Rendering:
┌──────────┐ ┌─────┐ ┌──────────────┐
│██████████│ │░░░░░│ │██████████████│
└──────────┘ └─────┘ └──────────────┘
    Work     Handle      Resume
             click!      
User input processed immediately!

Profiler Component

The Profiler component measures rendering performance programmatically, collecting timing data for optimization analysis or performance monitoring in production.

import { Profiler } from 'react'; function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) { console.log({ id, // "Navigation" phase, // "mount" | "update" actualDuration, // Time spent rendering (ms) baseDuration, // Estimated time without memoization startTime, // When React started rendering commitTime, // When React committed the update }); } function App() { return ( <Profiler id="Navigation" onRender={onRender}> <Navigation /> </Profiler> ); }

React DevTools Profiler

React DevTools Profiler is a browser extension feature that visually displays component render times, highlights unnecessary re-renders, and shows what triggered each update.

┌─────────────────────────────────────────────────────────┐
│  React DevTools - Profiler Tab                          │
├─────────────────────────────────────────────────────────┤
│  ⏺ Start Profiling    [Flamegraph] [Ranked] [Timeline] │
├─────────────────────────────────────────────────────────┤
│  Commit #3 (0.8ms)                                      │
│  ┌─────────────────────────────────────────────┐       │
│  │ App (0.1ms)                                  │       │
│  │  ├── Header (0.05ms) ✓ Did not render       │       │
│  │  ├── ProductList (0.6ms) ⚠ Rendered         │       │
│  │  │    └── ProductCard x 50 (0.5ms)          │       │
│  │  └── Footer (0.05ms) ✓ Did not render       │       │
│  └─────────────────────────────────────────────┘       │
│  Why did this render? Props changed: (items)            │
└─────────────────────────────────────────────────────────┘

Performance Monitoring

Performance monitoring uses tools like Web Vitals, React Profiler, and APM services to track real-user metrics (LCP, FID, CLS) and identify performance regressions in production.

import { useReportWebVitals } from 'next/web-vitals'; // Or use web-vitals directly import { getCLS, getFID, getLCP } from 'web-vitals'; function App() { useEffect(() => { getCLS(console.log); // Cumulative Layout Shift getFID(console.log); // First Input Delay getLCP(console.log); // Largest Contentful Paint // Send to analytics getCLS(metric => sendToAnalytics('CLS', metric.value)); }, []); return <MainApp />; }

Bundle Size Optimization

Bundle size optimization involves analyzing and reducing JavaScript bundle sizes through techniques like code splitting, tree shaking, dependency auditing, and replacing heavy libraries with lighter alternatives.

# Analyze bundle npm install --save-dev webpack-bundle-analyzer npx vite-bundle-visualizer # For Vite # Check package sizes before installing npx bundlephobia-cli lodash
// ❌ Bad: imports entire library (70KB) import _ from 'lodash'; _.debounce(fn, 300); // ✅ Good: imports only needed function (2KB) import debounce from 'lodash/debounce'; debounce(fn, 300); // ✅ Better: use native or tiny alternative import debounce from 'just-debounce-it'; // 200B

Tree Shaking

Tree shaking eliminates unused code during bundling by analyzing ES module imports/exports, automatically removing dead code paths and unused exports to reduce bundle size.

// math.js export const add = (a, b) => a + b; export const subtract = (a, b) => a - b; export const multiply = (a, b) => a * b; // Unused export const divide = (a, b) => a / b; // Unused // app.js import { add, subtract } from './math'; // Only these imported console.log(add(1, 2)); console.log(subtract(5, 3)); // After tree shaking: multiply and divide are removed from bundle!
┌───────────────────┐     Tree      ┌───────────────────┐
│ ● add             │    Shaking    │ ● add             │
│ ● subtract        │  ──────────>  │ ● subtract        │
│ ○ multiply (dead) │              │                   │
│ ○ divide (dead)   │              │                   │
└───────────────────┘              └───────────────────┘
     Source                           Bundle

React 18+ Features

Concurrent Rendering

Concurrent rendering allows React to work on multiple versions of the UI simultaneously, interrupt rendering to handle high-priority updates, and discard stale work—fundamentally enabling features like useTransition and Suspense for data fetching.

// React 18 automatically uses concurrent features when you use: // - useTransition // - useDeferredValue // - Suspense for data fetching // - startTransition // Enable by using createRoot (React 18+) import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render(<App />); // Legacy mode (no concurrent features): // ReactDOM.render(<App />, document.getElementById('root'));

Automatic Batching

React 18 automatically batches multiple state updates into a single re-render, even inside promises, timeouts, and native event handlers—previously, batching only worked in React event handlers.

// React 17: Multiple re-renders setTimeout(() => { setCount(c => c + 1); // Re-render setFlag(f => !f); // Re-render }, 1000); // React 18: Single re-render (automatic batching!) setTimeout(() => { setCount(c => c + 1); // Batched setFlag(f => !f); // Batched → One render! }, 1000); // Opt-out if needed: import { flushSync } from 'react-dom'; flushSync(() => setCount(c => c + 1)); // Immediate render flushSync(() => setFlag(f => !f)); // Immediate render
React 17:                    React 18:
setState A → Render         setState A ─┐
setState B → Render         setState B ─┼→ Single Render
setState C → Render         setState C ─┘
(3 renders)                 (1 render!)

Transitions (useTransition, startTransition)

Transitions mark state updates as non-urgent, telling React it's okay to interrupt them for more important updates like typing—keeping the UI responsive during expensive operations.

import { useTransition, startTransition } from 'react'; function TabContainer() { const [tab, setTab] = useState('home'); const [isPending, startTransition] = useTransition(); const selectTab = (nextTab) => { startTransition(() => { setTab(nextTab); // Non-urgent, can be interrupted }); }; return ( <> <TabButtons onSelect={selectTab} /> <div style={{ opacity: isPending ? 0.7 : 1 }}> {isPending && <Spinner />} <TabContent tab={tab} /> </div> </> ); } // Without hook (fire and forget): startTransition(() => { setSearchResults(filterLargeList(query)); });

Suspense Improvements

React 18 expands Suspense to work with server-side rendering (streaming SSR), enables selective hydration, and allows multiple suspense boundaries to coordinate loading states more effectively.

// Nested Suspense with coordinated loading function App() { return ( <Suspense fallback={<PageSkeleton />}> <Header /> <Suspense fallback={<SidebarSkeleton />}> <Sidebar /> </Suspense> <Suspense fallback={<ContentSkeleton />}> <MainContent /> <Suspense fallback={<CommentsSkeleton />}> <Comments /> </Suspense> </Suspense> </Suspense> ); }
Streaming Order:
┌──────────────────────────────────────┐
│ [Header loads first]                 │  ← Immediate
├────────────┬─────────────────────────┤
│ [Sidebar]  │ [Content skeleton]      │  ← Sidebar streams in
│            │                         │
│            │ [Main content loads]    │  ← Content streams
│            │ [Comments skeleton]     │
│            │ [Comments load last]    │  ← Comments stream
└────────────┴─────────────────────────┘

Streaming SSR

Streaming SSR sends HTML progressively as components render on the server, showing content to users faster instead of waiting for the entire page to render before sending anything.

// server.js (simplified) import { renderToPipeableStream } from 'react-dom/server'; app.get('/', (req, res) => { const { pipe } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); pipe(res); // Start streaming immediately! }, }); });
Traditional SSR:          Streaming SSR:
[Wait...........]         [Header] ───────> Browser
[Wait...........]         [Nav]    ───────> Browser
[Wait...........]         [Content]───────> Browser
[Send all at once]        [Footer] ───────> Browser
       ↓                         ↓
   Slow TTFB               Fast TTFB, progressive render

Selective Hydration

Selective hydration prioritizes hydrating components users are interacting with, rather than hydrating the entire page in order—making interactive elements responsive faster.

// User clicks Comments before it's hydrated // React 18 will prioritize hydrating Comments first! <Suspense fallback={<HeavyChartSkeleton />}> <HeavyChart /> {/* Hydrates in background */} </Suspense> <Suspense fallback={<CommentsSkeleton />}> <Comments /> {/* User clicked! Hydrates first */} </Suspense>
Traditional Hydration:
[Header] → [Nav] → [Content] → [Comments] → [Footer]
                                    ↑
                               User clicked here,
                               but must wait!

Selective Hydration:
[Header] → [Comments!] → [Nav] → [Content] → [Footer]
               ↑
         User clicked,
         prioritized immediately!

useDeferredValue

useDeferredValue creates a deferred version of a value that lags behind during urgent updates, allowing expensive re-renders to be interrupted while keeping input responsive.

import { useDeferredValue, useMemo } from 'react'; function SearchResults({ query }) { // deferredQuery may lag behind query during typing const deferredQuery = useDeferredValue(query); // Expensive filtering uses deferred value const results = useMemo( () => filterLargeDataset(deferredQuery), [deferredQuery] ); // Visual feedback when stale const isStale = query !== deferredQuery; return ( <ul style={{ opacity: isStale ? 0.5 : 1 }}> {results.map(item => <li key={item.id}>{item.name}</li>)} </ul> ); }

useId

useId generates unique, stable IDs that are consistent between server and client rendering, solving hydration mismatches when generating IDs for accessibility attributes.

import { useId } from 'react'; function FormField({ label }) { const id = useId(); // e.g., ":r1:" return ( <> <label htmlFor={id}>{label}</label> <input id={id} type="text" /> </> ); } // Multiple IDs in same component function PasswordField() { const id = useId(); return ( <> <label htmlFor={`${id}-password`}>Password</label> <input id={`${id}-password`} type="password" /> <p id={`${id}-hint`}>Must be 8+ characters</p> <input aria-describedby={`${id}-hint`} /> </> ); }

useSyncExternalStore

useSyncExternalStore subscribes to external stores (Redux, Zustand, browser APIs) safely with concurrent rendering, preventing tearing where parts of UI show inconsistent data.

import { useSyncExternalStore } from 'react'; // Subscribe to browser online status function useOnlineStatus() { return useSyncExternalStore( subscribe, // How to subscribe getSnapshot, // Get current value (client) getServerSnapshot // Get value during SSR ); } function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; } const getSnapshot = () => navigator.onLine; const getServerSnapshot = () => true; // Assume online on server function StatusBar() { const isOnline = useOnlineStatus(); return <div>{isOnline ? '✅ Online' : '❌ Offline'}</div>; }

useInsertionEffect

useInsertionEffect fires synchronously before DOM mutations, designed specifically for CSS-in-JS libraries to inject styles before any layout effects read the DOM.

import { useInsertionEffect } from 'react'; // For CSS-in-JS library authors only! function useCSS(rule) { useInsertionEffect(() => { const style = document.createElement('style'); style.textContent = rule; document.head.appendChild(style); return () => document.head.removeChild(style); }, [rule]); } // Effect execution order: // 1. useInsertionEffect ← Inject styles (before paint) // 2. useLayoutEffect ← Read layout // 3. useEffect ← After paint
┌────────────────────────────────────────────────────┐
│              Effect Timing Order                    │
├────────────────────────────────────────────────────┤
│  Render → useInsertionEffect → DOM Mutation        │
│                                    ↓                │
│                          useLayoutEffect            │
│                                    ↓                │
│                              Browser Paint          │
│                                    ↓                │
│                               useEffect             │
└────────────────────────────────────────────────────┘

New Strict Mode Behaviors

React 18's Strict Mode double-invokes effects to help identify impure components and missing cleanup, simulating the component mounting, unmounting, and remounting—exposing bugs before they hit production.

// React 18 Strict Mode in Development: // 1. Component mounts // 2. Effects run // 3. Cleanup functions run (simulated unmount) // 4. Effects run again (simulated remount) function Component() { useEffect(() => { console.log('Effect ran'); // Logs twice in StrictMode! return () => console.log('Cleanup'); }, []); return <div>Hello</div>; } // Console output in StrictMode: // "Effect ran" // "Cleanup" // "Effect ran" // This catches bugs like missing cleanup or side effects that // aren't idempotent (safe to run multiple times)

Server Components (experimental)

React Server Components (RSC) render exclusively on the server with zero JavaScript sent to the client, enabling direct database/filesystem access while dramatically reducing bundle sizes for data-heavy components.

// ServerComponent.server.jsx - Runs ONLY on server import db from './database'; async function ServerComponent() { // Direct database access - no API needed! const products = await db.query('SELECT * FROM products'); return ( <ul> {products.map(p => <li key={p.id}>{p.name}</li>)} </ul> ); // Zero JS shipped for this component! } // ClientComponent.client.jsx - Runs on client 'use client'; function ClientComponent() { const [count, setCount] = useState(0); // Interactive, ships JavaScript return <button onClick={() => setCount(c + 1)}>{count}</button>; }

React Server Components (RSC)

RSC architecture splits components between server and client, with server components fetching data and rendering HTML while client components handle interactivity—fully implemented in Next.js 13+ App Router.

┌─────────────────────────────────────────────────────────────┐
│                 RSC Architecture                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Server                           Client                   │
│  ┌─────────────────────┐         ┌───────────────────┐     │
│  │  ServerComponent    │   ──>   │   HTML (static)   │     │
│  │  - Async/await      │         │   No JavaScript   │     │
│  │  - DB access        │         │                   │     │
│  │  - File system      │         │                   │     │
│  │  - Zero bundle      │         └───────────────────┘     │
│  └─────────────────────┘                                    │
│                                                             │
│  ┌─────────────────────┐         ┌───────────────────┐     │
│  │  'use client'       │   ──>   │  Interactive JS   │     │
│  │  ClientComponent    │         │  - useState       │     │
│  │  - Event handlers   │         │  - useEffect      │     │
│  │  - Browser APIs     │         │  - Event handlers │     │
│  └─────────────────────┘         └───────────────────┘     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
// app/page.jsx (Next.js 13+ - Server Component by default) import { ClientButton } from './ClientButton'; async function Page() { const data = await fetch('https://api.example.com/data'); return ( <main> <h1>Server Rendered</h1> <DataDisplay data={data} /> {/* Server Component */} <ClientButton /> {/* Client Component */} </main> ); }

Testing React

Testing Philosophy

React testing follows the "Testing Trophy" philosophy: prioritize integration tests over unit tests, test behavior rather than implementation details, and write tests that resemble how users interact with your app. The guiding principle is "the more your tests resemble the way your software is used, the more confidence they give you."

        /\        E2E (few)
       /  \
      /----\      Integration (many)
     /      \
    /--------\    Unit (some)
   /          \
  /------------\  Static (ESLint/TS)

Jest Basics

Jest is the default test runner for React, providing test suites, assertions, mocking, and code coverage out of the box with zero configuration.

describe('Calculator', () => { test('adds two numbers', () => { expect(add(2, 3)).toBe(5); }); test('throws on invalid input', () => { expect(() => add('a', 1)).toThrow(); }); });

React Testing Library

RTL provides utilities to test React components by querying the DOM the same way users would—by text, label, role—encouraging accessible and maintainable tests without testing implementation details.

import { render, screen } from '@testing-library/react'; import UserProfile from './UserProfile'; test('displays user name', () => { render(<UserProfile name="Alice" />); expect(screen.getByText('Alice')).toBeInTheDocument(); });

Rendering Components

The render function mounts your component to a virtual DOM (jsdom), returning utilities to query and interact with it; always clean up happens automatically between tests.

import { render, screen } from '@testing-library/react'; // Basic render const { container, unmount, rerender } = render(<Button label="Click" />); // Render with providers (router, theme, etc.) render(<Button />, { wrapper: ThemeProvider }); // Rerender with new props rerender(<Button label="Updated" />);

Querying Elements

RTL provides a priority-based query system: getBy (throws if not found), queryBy (returns null), and findBy (async); prefer accessible queries like ByRole, ByLabelText, ByText.

// Query Priority (best to worst) screen.getByRole('button', { name: /submit/i }) // ✅ Best screen.getByLabelText('Email') // ✅ Great screen.getByText('Welcome') // ✅ Good screen.getByTestId('submit-btn') // ⚠️ Last resort // Query types screen.getByRole('button') // throws if missing screen.queryByRole('button') // returns null if missing screen.findByRole('button') // async, waits for element

Firing Events

fireEvent dispatches DOM events to simulate user interactions; it triggers the event directly without simulating the full browser behavior chain.

import { render, screen, fireEvent } from '@testing-library/react'; test('increments counter on click', () => { render(<Counter />); const button = screen.getByRole('button', { name: /increment/i }); fireEvent.click(button); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); // Other events fireEvent.change(input, { target: { value: 'hello' } }); fireEvent.submit(form); fireEvent.keyDown(element, { key: 'Enter', code: 'Enter' });

Async Utilities (waitFor, findBy)

These utilities handle asynchronous operations: findBy queries wait for elements to appear, waitFor retries assertions until they pass or timeout, essential for testing data fetching and animations.

// findBy - combines getBy + waitFor const element = await screen.findByText('Loaded Data'); // waitFor - retry until assertion passes await waitFor(() => { expect(screen.getByText('Success')).toBeInTheDocument(); }); // waitForElementToBeRemoved await waitForElementToBeRemoved(() => screen.queryByText('Loading...')); // Configure timeout await waitFor(() => expect(mockFn).toHaveBeenCalled(), { timeout: 3000 });

User Event Library

@testing-library/user-event provides more realistic event simulation than fireEvent, accurately mimicking browser behavior including focus, keyboard events, and event sequencing.

import userEvent from '@testing-library/user-event'; test('form submission flow', async () => { const user = userEvent.setup(); render(<LoginForm />); await user.type(screen.getByLabelText('Email'), 'test@example.com'); await user.type(screen.getByLabelText('Password'), 'secret123'); await user.click(screen.getByRole('button', { name: /login/i })); // user.type triggers: focus, keyDown, keyPress, input, keyUp for EACH char // user.click triggers: pointerDown, mouseDown, pointerUp, mouseUp, click });

Testing Hooks (@testing-library/react-hooks)

For React 18+, use renderHook from @testing-library/react directly to test custom hooks in isolation, returning the hook's result and utilities to trigger updates.

import { renderHook, act } from '@testing-library/react'; import useCounter from './useCounter'; test('useCounter increments', () => { const { result } = renderHook(() => useCounter(0)); expect(result.current.count).toBe(0); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); // With wrapper for context const { result } = renderHook(() => useTheme(), { wrapper: ({ children }) => <ThemeProvider>{children}</ThemeProvider> });

Mocking

Jest's mocking system replaces modules, functions, or timers with controlled implementations, essential for isolating components from external dependencies and testing edge cases.

// Mock a function const mockCallback = jest.fn(); mockCallback.mockReturnValue(42); mockCallback.mockResolvedValue({ data: 'async' }); // Mock timers jest.useFakeTimers(); jest.advanceTimersByTime(1000); jest.runAllTimers(); // Spy on existing methods jest.spyOn(console, 'error').mockImplementation(() => {});

Mocking Modules

jest.mock() replaces entire module imports with mock implementations, useful for isolating components from heavy dependencies like routers, stores, or utility libraries.

// __mocks__/axios.js or inline jest.mock('axios'); // Mock specific exports jest.mock('./analytics', () => ({ trackEvent: jest.fn(), initAnalytics: jest.fn(), })); // Partial mock (keep some real implementations) jest.mock('./utils', () => ({ ...jest.requireActual('./utils'), formatDate: jest.fn(() => '2024-01-01'), })); // Mock react-router jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: () => jest.fn(), }));

Mocking API Calls

Use Mock Service Worker (MSW) to intercept network requests at the service worker level, providing realistic API mocking that works across all fetching libraries.

// src/mocks/handlers.js import { rest } from 'msw'; export const handlers = [ rest.get('/api/users', (req, res, ctx) => { return res(ctx.json([{ id: 1, name: 'Alice' }])); }), rest.post('/api/login', async (req, res, ctx) => { const { email } = await req.json(); if (email === 'fail@test.com') { return res(ctx.status(401), ctx.json({ error: 'Invalid' })); } return res(ctx.json({ token: 'abc123' })); }), ]; // setupTests.js import { setupServer } from 'msw/node'; const server = setupServer(...handlers); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close());

Test Coverage

Jest collects coverage metrics showing which lines, branches, functions, and statements are executed during tests; aim for meaningful coverage (70-80%) rather than 100%.

# Run with coverage jest --coverage # Coverage report output --------------------|---------|----------|---------|---------| File | % Stmts | % Branch | % Funcs | % Lines | --------------------|---------|----------|---------|---------| All files | 85.71 | 83.33 | 77.78 | 86.96 | Button.tsx | 100 | 100 | 100 | 100 | Form.tsx | 71.43 | 50 | 66.67 | 71.43 | --------------------|---------|----------|---------|---------| # jest.config.js coverageThreshold: { global: { branches: 80, functions: 80, lines: 80 } }

Snapshot Testing

Snapshots capture a component's rendered output and compare against future runs; useful for detecting unintended UI changes but can become brittle if overused.

import renderer from 'react-test-renderer'; test('Button renders correctly', () => { const tree = renderer.create(<Button label="Click me" />).toJSON(); expect(tree).toMatchSnapshot(); }); // Inline snapshots (stored in test file) test('formats name', () => { expect(formatName('alice')).toMatchInlineSnapshot(`"Alice"`); }); // Update snapshots when intentional changes occur // jest --updateSnapshot or press 'u' in watch mode

Integration Tests

Integration tests verify multiple components working together, testing realistic user flows through your application while mocking only external boundaries (APIs, browser APIs).

test('complete checkout flow', async () => { const user = userEvent.setup(); render( <CartProvider> <Router> <App /> </Router> </CartProvider> ); // Add item to cart await user.click(screen.getByRole('button', { name: /add to cart/i })); // Navigate to checkout await user.click(screen.getByRole('link', { name: /cart/i })); expect(screen.getByText('1 item')).toBeInTheDocument(); // Complete purchase await user.click(screen.getByRole('button', { name: /checkout/i })); await screen.findByText('Order confirmed!'); });

E2E Testing with Cypress/Playwright

End-to-end tests run in real browsers, testing the complete application stack including backend integrations; slower but highest confidence for critical user journeys.

// Cypress: cypress/e2e/login.cy.js describe('Login', () => { it('logs in successfully', () => { cy.visit('/login'); cy.get('[data-testid="email"]').type('user@test.com'); cy.get('[data-testid="password"]').type('password123'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); cy.contains('Welcome back').should('be.visible'); }); }); // Playwright: tests/login.spec.ts test('logs in successfully', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('user@test.com'); await page.getByLabel('Password').fill('password123'); await page.getByRole('button', { name: 'Login' }).click(); await expect(page).toHaveURL(/dashboard/); });

Component Testing

Cypress and Playwright now support mounting individual React components in isolation, combining the realism of browser testing with the speed of unit tests.

// Cypress Component Testing: Button.cy.tsx import Button from './Button'; describe('Button', () => { it('handles click events', () => { const onClickSpy = cy.spy().as('onClick'); cy.mount(<Button onClick={onClickSpy}>Click me</Button>); cy.get('button').click(); cy.get('@onClick').should('have.been.calledOnce'); }); }); // Playwright Component Testing test('button click', async ({ mount }) => { let clicked = false; const component = await mount( <Button onClick={() => clicked = true}>Click</Button> ); await component.click(); expect(clicked).toBe(true); });