XplormityXplormity
HomeHandbooks
Browse
Xplormity

TLDR developer handbooks for
seasoned developers.

Handbooks

RustNestJSNext.jsGitDockerTypeScriptReactNode.jsDSASQLSystem DesignTailwind CSS

Site

HomeHandbooksAboutPrivacyTerms

Connect

GitHubTwitterLinkedIn

© 2026 Xplormity. All rights reserved.

HandbooksReactReact Performance & Patterns

React Performance & Patterns

performancepatternsmemoizationrenderinghandbook

TL;DR

  • React re-renders a component when its state changes or parent re-renders.
  • Prevent unnecessary renders: React.memo, useMemo, useCallback (or let the Compiler handle it).
  • Key patterns: Composition over inheritance, render props, compound components, controlled/uncontrolled.
  • Profiling: React DevTools Profiler, <Profiler> component, why-did-you-render.

Step 1: Understanding Re-Renders

React's re-rendering model is the single most misunderstood aspect of the library. It was designed for correctness first: when state changes, re-render everything below to guarantee the UI matches the data. This simplifies mental models but creates performance problems at scale — a state change in a parent can trigger re-renders in hundreds of children that don't need updating. Understanding exactly what triggers re-renders (and what doesn't) is the foundation of React performance work and one of the most common interview topics.

// A component re-renders when:
// 1. Its state changes (setState)
// 2. Its parent re-renders (unless memoized)
// 3. Its context value changes

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <Child /> {/* Re-renders EVERY time Parent re-renders! */}
    </div>
  );
}

function Child() {
  console.log('Child rendered'); // Logs on every parent state change
  return <p>I'm expensive</p>;
}

Fix: Move State Down

// ✅ Only the Counter re-renders, not the expensive child
function Parent() {
  return (
    <div>
      <Counter /> {/* State is isolated here */}
      <ExpensiveChild />
    </div>
  );
}

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </>
  );
}

Fix: Children as Props (Composition)

// ✅ children don't re-render when Parent's state changes
function Parent({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      {children} {/* Already created by grandparent — won't re-render */}
    </div>
  );
}

// Usage
<Parent>
  <ExpensiveChild /> {/* Stays stable! */}
</Parent>

Step 2: Memoization Techniques

Memoization in React exists because the default "re-render everything" approach becomes expensive when components do heavy computation or receive stable props from re-rendering parents. React.memo, useMemo, and useCallback were React's manual optimization tools — they tell React "skip this work if inputs haven't changed". In React 19, the Compiler automates most of this, but understanding the underlying concept is still essential for debugging performance issues and for codebases that haven't adopted the Compiler yet.

React.memo — Skip Re-Renders

// Only re-renders when props actually change (shallow comparison)
const MemoizedChild = React.memo(function Child({ name }: { name: string }) {
  console.log('Child rendered');
  return <p>Hello {name}</p>;
});

// Custom comparison
const MemoizedList = React.memo(
  function List({ items }: { items: Item[] }) { /* ... */ },
  (prevProps, nextProps) => prevProps.items.length === nextProps.items.length
);

useMemo — Cache Expensive Calculations

function Dashboard({ data }: { data: DataPoint[] }) {
  // ✅ Only recalculates when `data` changes
  const statistics = useMemo(() => {
    return {
      total: data.reduce((sum, d) => sum + d.value, 0),
      average: data.reduce((sum, d) => sum + d.value, 0) / data.length,
      max: Math.max(...data.map(d => d.value)),
    };
  }, [data]);

  return <StatsDisplay stats={statistics} />;
}

useCallback — Stable Function References

function SearchPage() {
  const [query, setQuery] = useState('');

  // ✅ Stable reference — won't cause child to re-render
  const handleSearch = useCallback((term: string) => {
    fetchResults(term);
  }, []);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SearchResults onSearch={handleSearch} />
    </div>
  );
}

Step 3: Virtualization — Render Large Lists

Virtualization was invented because rendering 10,000 DOM elements at once is catastrophically slow — each element costs memory and layout computation. The insight: if the viewport only shows 20 items, only render those 20 and swap them as the user scrolls. Libraries like react-virtual make this seamless. You'll need virtualization for any list/table with 500+ items: chat logs, data tables, infinite feeds, file explorers. Without it, initial render can take 3+ seconds; with it, it's always instant regardless of data size.

// Using @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: string[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,     // Could be 100,000+
    getScrollElement: () => parentRef.current,
    estimateSize: () => 45,  // Estimated row height
  });

  return (
    <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualRow.start}px)`,
              height: `${virtualRow.size}px`,
            }}
          >
            {items[virtualRow.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

Step 4: Code Splitting & Lazy Loading

Code splitting exists because modern React apps can easily reach 2-5 MB of JavaScript, but users only need a fraction on any given page. Without splitting, users download the admin dashboard, settings page, and every route upfront — even if they never visit them. React.lazy + Suspense lets you split your bundle at route boundaries (or any component boundary), loading code on-demand. This directly impacts Core Web Vitals (LCP, TTI) and is a standard expectation for production React apps.

import { lazy, Suspense } from 'react';

// Component loaded only when needed
const AdminDashboard = lazy(() => import('./AdminDashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} /> {/* Always loaded */}
      <Route
        path="/admin"
        element={
          <Suspense fallback={<Spinner />}>
            <AdminDashboard /> {/* Loaded on first visit */}
          </Suspense>
        }
      />
      <Route
        path="/settings"
        element={
          <Suspense fallback={<Spinner />}>
            <Settings />
          </Suspense>
        }
      />
    </Routes>
  );
}

Preloading on Hover

const AdminDashboard = lazy(() => import('./AdminDashboard'));

function NavLink() {
  const preload = () => import('./AdminDashboard'); // Start loading

  return (
    <Link
      to="/admin"
      onMouseEnter={preload} // Preload when user hovers
      onFocus={preload}
    >
      Admin
    </Link>
  );
}

Step 5: Key Design Patterns

These patterns (compound components, controlled/uncontrolled, custom hooks) emerged from years of React community experience building reusable UI libraries. They solve the tension between flexibility and simplicity: how do you make a component configurable without passing 30 props? Compound components (like <Tabs> + <Tabs.Panel>) let consumers compose behavior. Controlled/uncontrolled patterns let you choose who owns the state. These are the patterns you'll find in every major UI library (Radix, Headless UI, Shadcn) and interviewers love asking about them.

Compound Components

// API: <Tabs> + <Tabs.Tab> + <Tabs.Panel>
const TabsContext = createContext<{ activeTab: string; setActive: (id: string) => void } | null>(null);

function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
  const [activeTab, setActive] = useState(defaultTab);
  return (
    <TabsContext.Provider value={{ activeTab, setActive }}>
      <div>{children}</div>
    </TabsContext.Provider>
  );
}

Tabs.Tab = function Tab({ id, children }: { id: string; children: ReactNode }) {
  const { activeTab, setActive } = useContext(TabsContext)!;
  return (
    <button
      onClick={() => setActive(id)}
      className={activeTab === id ? 'active' : ''}
    >
      {children}
    </button>
  );
};

Tabs.Panel = function Panel({ id, children }: { id: string; children: ReactNode }) {
  const { activeTab } = useContext(TabsContext)!;
  return activeTab === id ? <div>{children}</div> : null;
};

// Usage
<Tabs defaultTab="profile">
  <Tabs.Tab id="profile">Profile</Tabs.Tab>
  <Tabs.Tab id="settings">Settings</Tabs.Tab>
  <Tabs.Panel id="profile"><ProfileContent /></Tabs.Panel>
  <Tabs.Panel id="settings"><SettingsContent /></Tabs.Panel>
</Tabs>

Controlled vs Uncontrolled

// Uncontrolled — component manages own state
function UncontrolledInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  const handleSubmit = () => console.log(inputRef.current?.value);
  return <input ref={inputRef} defaultValue="initial" />;
}

// Controlled — parent manages state
function ControlledInput() {
  const [value, setValue] = useState('');
  return (
    <input
      value={value}
      onChange={e => setValue(e.target.value)}
    />
  );
}

Custom Hook Pattern (Extract Logic)

// Extract reusable logic into hooks
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  return { value, toggle, setTrue, setFalse };
}

function Modal() {
  const { value: isOpen, toggle, setFalse: close } = useToggle();
  return (
    <>
      <button onClick={toggle}>Open Modal</button>
      {isOpen && <ModalContent onClose={close} />}
    </>
  );
}

Step 6: Error Boundaries

Error boundaries exist because a JavaScript error in one component shouldn't crash the entire app. Before error boundaries, a thrown error in any component would unmount the whole React tree — showing users a blank white screen. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of their child tree, letting you show a graceful fallback UI instead of a crash. They're a production requirement for any serious React app, typically placed around route boundaries, feature sections, and third-party widget integrations.

import { Component, ErrorInfo, ReactNode } from 'react';

class ErrorBoundary extends Component<
  { children: ReactNode; fallback: ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    console.error('Error caught:', error, info.componentStack);
    // Send to error reporting service
  }

  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary fallback={<p>Something went wrong</p>}>
      <Dashboard />
    </ErrorBoundary>
  );
}

Interview Questions

  1. How does React decide when to re-render?

    • State change → re-render that component + all children. Context change → re-render all consumers. Parent re-render → children re-render unless memoized. Props change is NOT a trigger — parent re-rendering is.
  2. What's the difference between useMemo and useCallback?

    • useMemo caches a computed value. useCallback caches a function reference. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
  3. When should you NOT use React.memo?

    • When props change on every render anyway (inline objects/functions without useCallback). When the component is cheap to render. When it renders differently most of the time (memo comparison adds overhead).
  4. Explain the compound component pattern.

    • Multiple related components share implicit state via Context. The parent manages shared state, children read from it. Gives flexible API (<Tabs>, <Tabs.Tab>) without prop drilling.
React 19 — New Features & PatternsState Management — Zustand, Context & TanStack Query

On this page

  • TL;DR
  • Step 1: Understanding Re-Renders
  • Fix: Move State Down
  • Fix: Children as Props (Composition)
  • Step 2: Memoization Techniques
  • React.memo — Skip Re-Renders
  • useMemo — Cache Expensive Calculations
  • useCallback — Stable Function References
  • Step 3: Virtualization — Render Large Lists
  • Step 4: Code Splitting & Lazy Loading
  • Preloading on Hover
  • Step 5: Key Design Patterns
  • Compound Components
  • Controlled vs Uncontrolled
  • Custom Hook Pattern (Extract Logic)
  • Step 6: Error Boundaries
  • Interview Questions