MindForge
MindForge
DashboardHelp & Support
Dashboard
MindForge

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
React Performance
Course contents

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Browsing without an account — progress is saved in this browser only.Sign in to save it

React Performance

Reading45 min

"How would you optimize a slow React app?" is a near-guaranteed senior-level question, and "wrap everything in useMemo" is the wrong answer. React performance questions have also shifted in recent years, from just "when do you use useMemo" to "what does automatic batching change" and "when do you reach for useTransition versus useDeferredValue." This lesson covers React.memo, useMemo, and useCallback correctly, including the real cost of memoization that most candidates skip entirely, how to measure before you optimize anything, and the concurrent-rendering APIs that make modern React feel fast under load.

When memoization is worth reaching for

From the rendering-fundamentals lesson: a parent re-rendering re-renders every child by default, and re-rendering a function component just means calling the function again to produce a new VDOM subtree, which reconciliation then diffs. For cheap components that's essentially free, diffing a <span> costs nanoseconds. Memoization only pays off when re-rendering is measurably expensive: large lists, heavy computation inside the render body, or a component with an expensive subtree whose props are stable most of the time.

React.memo skips re-rendering a component if its props are shallowly equal to last time:

tsx
type RowProps = { id: string; label: string; onSelect: (id: string) => void };
const ExpensiveRow = React.memo(function ExpensiveRow({ id, label, onSelect }: RowProps) {
  console.log('rendering row', id);
  return <div onClick={() => onSelect(id)}>{label}</div>;
});

React.memo does a shallow Object.is comparison per prop, which is exactly why it silently does nothing in the most common real scenario: an inline arrow function or object literal passed as a prop is a new reference every render, so the shallow comparison reports "changed" every single time and memo never actually skips anything.

tsx
function ParentBroken() {
  const [count, setCount] = useState(0);
  // New function reference every render → ExpensiveRow's memo check always fails
  return <ExpensiveRow id="1" label="Row" onSelect={(id) => console.log(id)} />;
}

useMemo caches the result of a computation between renders, recomputing only when its dependency array changes:

tsx
function ProductList({ products, filterText }: { products: Product[]; filterText: string }) {
  const filtered = useMemo(
    () => products.filter(p => p.name.toLowerCase().includes(filterText.toLowerCase())),
    [products, filterText],
  );
  return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

useCallback caches a function reference between renders, so passing it as a prop doesn't defeat a child's React.memo. It's useMemo specialized for functions: useCallback(fn, deps) is exactly useMemo(() => fn, deps).

tsx
function ParentFixed() {
  const handleSelect = useCallback((id: string) => { console.log('selected', id); }, []);
  return <ExpensiveRow id="1" label="Row" onSelect={handleSelect} />;
  // Now ExpensiveRow's React.memo check actually passes when unrelated state changes elsewhere
}

The rule of thumb worth internalizing: useMemo/useCallback on a value only pays off if the component receiving it is also wrapped in React.memo, or the value feeds into another hook's dependency array. Memoizing a callback passed to a plain, non-memoized child changes nothing, that child re-renders regardless of whether the reference is stable.

Remember: React.memo compares props by reference. A fresh inline function or object literal every render defeats it silently, unless the parent also memoizes that value.

Sign in to try this knowledge check.

Memoization isn't free

This is what senior-level interviews are actually probing for. Every memoized value or function stays alive between renders, holding references to whatever its closure captured, real memory pressure at scale across thousands of memoized rows. useMemo/useCallback/React.memo all pay a comparison cost on every render too, dependency-array diffing or shallow prop comparison, and for a genuinely cheap computation, that comparison can cost more than just redoing the work would have. There's a maintenance cost as well: a wrong or incomplete dependency array is a correctness bug, a stale closure, not just a missed optimization. useMemo(() => expensive(a, b), [a]) silently keeps using a stale b forever the moment b changes without a changing alongside it.

tsx
// Don't: memoizing a cheap string concat costs more than it saves
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// Just do this
const fullName = `${firstName} ${lastName}`;

Should every component be wrapped in React.memo by default? No. React.memo on a component whose props change on nearly every render, or that's cheap to render regardless, adds a wasted comparison on top of the render you were trying to avoid. Reach for it when profiling shows a specific component re-rendering expensively with otherwise-stable props, not as a preemptive habit.

Remember: every memoization hook costs a comparison on every render. For a cheap computation, that comparison can cost more than just redoing the work.

Sign in to try this knowledge check.

Measuring before you touch anything

Don't guess. Two tools, the same underlying idea: record renders, see what's actually slow.

The React DevTools Profiler tab records a session as you interact with the app, and shows a flamegraph per commit, each bar a component, width equal to render time, with a "why did this render?" breakdown of exactly which prop, state, or context changed. It's the fastest way to confirm a suspected unnecessary re-render before you reach for memo at all.

The React.Profiler component does the same thing programmatically, and works in production too:

tsx
import { Profiler, type ProfilerOnRenderCallback } from 'react';

const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
  if (actualDuration > 16) { // longer than one 60fps frame budget
    console.warn(`${id} took ${actualDuration.toFixed(2)}ms during ${phase}`);
  }
};

function App() {
  return (
    <Profiler id="ProductList" onRender={onRender}>
      <ProductList products={products} filterText={filterText} />
    </Profiler>
  );
}

The Chrome DevTools Performance tab records everything on the page, JS execution, layout, paint, garbage collection, not just React, which is where to look when the bottleneck might not be React at all, a synchronous JSON.parse of a huge payload, or the layout-thrashing pattern from the CSS and rendering-performance lessons. Look for long yellow (scripting) or purple (rendering) bars, and check the "Bottom-Up" tab to find which function actually consumed the time, not just which one happened to be on top of the call stack when the recording captured it.

The workflow that reads as a strong answer to "how would you optimize a slow app": profile first, identify the specific expensive component or computation, apply the narrowest targeted fix, memo, useMemo, useCallback, virtualization, code splitting, then profile again to confirm the fix actually helped. Optimizing without measuring first is the wrong answer even in the cases where the fix happens to work anyway.

Remember: profile first, apply the narrowest fix, profile again. Optimizing without measuring is the wrong answer even when the fix happens to work.

Sign in to try this knowledge check.

Automatic batching

Before React 18, updates only batched inside React's own event handlers. The same two setState calls inside a setTimeout, a promise callback, or a raw addEventListener would each trigger a separate render. React 18+ batches everywhere, automatically.

tsx
function Example() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  function handleClick() {
    fetch("/api/data").then(() => {
      // React 18+: these batch into ONE re-render, even inside a promise callback
      setCount((c) => c + 1);
      setFlag((f) => !f);
    });
  }
  return <button onClick={handleClick}>{count}</button>;
}

What changed between React 17 and 18's batching, specifically? In 17, only updates inside React's synthetic event handlers batched; the same pair of setState calls inside a setTimeout or fetch().then() would fire two separate renders. React 18's createRoot makes batching automatic regardless of where the updates originate. The opt-out is flushSync, needed rarely, mostly when you genuinely need a synchronous, unbatched update, forcing a DOM measurement between two state changes being the usual case.

Remember: React 17 only batched inside its own event handlers. React 18 batches everywhere, including inside a setTimeout or a promise callback.

Sign in to try this knowledge check.

useTransition versus useDeferredValue

Both mark work as low-priority so the browser stays responsive to typing and clicking, but they solve different shapes of problem.

useTransition: you own the state setter, and want to mark the update it triggers as non-urgent, so React can interrupt it if something more urgent, another keystroke, arrives.

tsx
function TabContainer() {
  const [tab, setTab] = useState<"home" | "analytics">("home");
  const [isPending, startTransition] = useTransition();
  function selectTab(next: "home" | "analytics") {
    startTransition(() => { setTab(next); }); // low priority: React abandons it if the user clicks again first
  }
  return (
    <div>
      <button onClick={() => selectTab("home")}>Home</button>
      <button onClick={() => selectTab("analytics")}>Analytics</button>
      {isPending && <Spinner />}
      {tab === "home" ? <HomeTab /> : <AnalyticsTab />}
    </div>
  );
}

useDeferredValue: you don't control the setter, a value arrives from a parent or a fast-changing input, and you want a "lagging" copy of it that updates at low priority instead.

tsx
function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query); // lags behind `query` under load
  const isStale = query !== deferredQuery;
  const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
  return <ul style={{ opacity: isStale ? 0.5 : 1 }}>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>;
}

When would you pick one over the other? Use useTransition when you own the state update, calling setState yourself in response to a click, and want to mark that specific update as interruptible. Use useDeferredValue when you're just consuming a value you don't control the setter for, most commonly a fast-changing controlled input passed down to an expensive child, and want the expensive derived work computed at lower priority without touching where the value originates at all.

Remember: useTransition is for when you own the setter. useDeferredValue is for when you only own a value someone else is setting.

Sign in to try this knowledge check.

Suspense for data, and the promise-creation rule

Suspense lets a component "pause" rendering while it waits for data, showing a fallback instead of the component manually managing an isLoading flag. In React 19, this is what use() runs on for reading a promise during render.

tsx
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // suspends the component until the promise resolves
  return <h1>{user.name}</h1>;
}
function ProfilePage({ userPromise }: { userPromise: Promise<User> }) {
  return <Suspense fallback={<Skeleton />}><UserProfile userPromise={userPromise} /></Suspense>;
}

The promise has to be created outside the render that reads it, in a parent, a route loader, or a cache. Calling fetch() directly inside the component would create a fresh promise on every single render and suspend forever, the exact same rule as useEffect's dependency array: don't create an unstable reference inside the render you're trying to stabilize.

Remember: a promise read by use() has to be created outside the component that reads it, or it recreates and resuspends on every render.

Sign in to try this knowledge check.

Building an optimistic-update component

Optimistic updates apply the expected result immediately, before the server confirms it, and roll back on failure, which makes the UI feel instant for actions that almost always succeed: likes, toggles, adding an item.

tsx
function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
  const [todos, setTodos] = useState(initialTodos);
  const [isPending, startTransition] = useTransition();

  // useOptimistic layers a temporary, hopeful value on top of `todos` that
  // automatically reverts once the real state update lands, or on error, below.
  const [optimisticTodos, setOptimisticTodo] = useOptimistic(todos, (state, toggledId: string) =>
    state.map((t) => (t.id === toggledId ? { ...t, completed: !t.completed } : t)));

  function toggle(id: string) {
    startTransition(async () => {
      setOptimisticTodo(id); // instant UI update
      try {
        await toggleTodoOnServer(id);
        setTodos((prev) => prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))); // confirm: commit the real state
      } catch {
        // no-op — not updating `todos` means optimisticTodos reverts automatically once the transition settles
      }
    });
  }

  return (
    <ul>
      {optimisticTodos.map((todo) => (
        <li key={todo.id} style={{ opacity: isPending ? 0.6 : 1 }}>
          <input type="checkbox" checked={todo.completed} onChange={() => toggle(todo.id)} /> {todo.text}
        </li>
      ))}
    </ul>
  );
}

useOptimistic has to be called inside a useTransition or a form action, it needs an async boundary to know when to revert. On success, commit the real state; on failure, deliberately do nothing, and React reverts the optimistic overlay for you once the transition finishes settling.

Remember: useOptimistic reverts automatically once its transition settles, unless you commit a matching real state update to make the optimistic change permanent.

Sign in to try this knowledge check.

Building a debounced search input, with two layers of cancellation

tsx
function useDebouncedValue<T>(value: T, delayMs: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(timer); // cancel the pending update if value changes again
  }, [value, delayMs]);
  return debounced;
}

function SearchBox() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebouncedValue(query, 300);
  const [results, setResults] = useState<SearchResult[]>([]);

  useEffect(() => {
    if (!debouncedQuery) { setResults([]); return; }
    const controller = new AbortController();
    fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`, { signal: controller.signal })
      .then((res) => res.json()).then(setResults)
      .catch((err) => { if (err.name !== "AbortError") throw err; });
    return () => controller.abort(); // cancel an in-flight request if the query changes again
  }, [debouncedQuery]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
      <ul>{results.map((r) => <li key={r.id}>{r.title}</li>)}</ul>
    </div>
  );
}

Two distinct layers of cancellation are doing work here, and interviewers check for both. The setTimeout/clearTimeout pair debounces the state update; the AbortController cancels an in-flight request if a newer debounced query supersedes an older one still in flight. Without the abort, a slow response to an old query could arrive after a fast response to a newer one and silently overwrite correct results with stale ones, a request-waterfall race condition.

Would useDeferredValue work instead of a manual debounce here? Only partially. useDeferredValue defers rendering work, not the timing of a network request, so it's the right tool for expensive client-side filtering or rendering of results you already have in hand. It won't reduce the number of API calls fired at all, since it introduces no time delay, only a lower render priority. For actually cutting request volume, a time-based debounce is still the tool.

Remember: debouncing delays the state update; aborting cancels an in-flight request. A search box that only debounces can still show stale results from a slower, older request.

Sign in to try this knowledge check.

The common thread

The optimistic-update component and the debounced search both lean on the same underlying idea useTransition embodies directly: mark work as interruptible or delayed so the UI thread stays responsive to whatever the user does next. Carry that framing into an interview more than any single hook's exact signature. Concurrent React is fundamentally about scheduling priority, and batching, transitions, deferred values, and optimistic state are each a different lever on that same knob.

PreviousReact Hooks InternalsNextLab: React — Stateful Counter Component

Course progress

0%
0 of 177 modules complete

On this page

  • When memoization is worth reaching for
  • Memoization isn't free
  • Measuring before you touch anything
  • Automatic batching
  • useTransition versus useDeferredValue
  • Suspense for data, and the promise-creation rule
  • Building an optimistic-update component
  • Building a debounced search input, with two layers of cancellation
  • The common thread