Skip to content

Instantly share code, notes, and snippets.

@bgauryy
Created November 9, 2025 06:15
Show Gist options
  • Select an option

  • Save bgauryy/38bd4550bd010a8ec813253aacffa0a4 to your computer and use it in GitHub Desktop.

Select an option

Save bgauryy/38bd4550bd010a8ec813253aacffa0a4 to your computer and use it in GitHub Desktop.
React Advanced Concepts Used WIth Octocode MCP

React Advanced Concepts: Deep Dive Research

This document provides an in-depth analysis of advanced React concepts based on research from the official React repository (facebook/react).

Table of Contents

  1. Fiber Architecture
  2. Concurrent Mode
  3. Suspense for Data Fetching
  4. Passive Effects vs Layout Effects
  5. Hydration
  6. useTransition
  7. FlushSync and Deferred Updates
  8. Error Boundaries
  9. React.memo vs useMemo vs useCallback
  10. Context Re-renders

1. Fiber Architecture

Overview

Fiber is React's reconciliation algorithm introduced in React 16. It's the core of how React updates the UI efficiently by breaking work into units that can be paused, prioritized, and resumed.

The Fiber Data Structure

Based on the React source code (packages/react-reconciler/src/ReactFiber.js), a Fiber node is a JavaScript object that represents a unit of work:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance properties
  this.tag = tag;                    // Type of component (function, class, etc.)
  this.key = key;                    // Unique key for reconciliation
  this.elementType = null;           // The type of element
  this.type = null;                  // The function/class itself
  this.stateNode = null;            // Reference to DOM node or instance

  // Fiber tree structure (linked list)
  this.return = null;               // Parent fiber
  this.child = null;                // First child fiber
  this.sibling = null;              // Next sibling fiber
  this.index = 0;                   // Position in parent's children

  // Refs
  this.ref = null;
  this.refCleanup = null;

  // Work-in-progress properties
  this.pendingProps = pendingProps; // New props from React element
  this.memoizedProps = null;        // Props from previous render
  this.updateQueue = null;          // Queue of state updates
  this.memoizedState = null;        // State from previous render
  this.dependencies = null;         // Context/subscription dependencies

  this.mode = mode;                 // Concurrent, Strict, Profile modes

  // Effects (side effects to perform)
  this.flags = NoFlags;             // Effect flags for this fiber
  this.subtreeFlags = NoFlags;      // Effect flags for subtree
  this.deletions = null;            // Children to delete

  // Priority lanes
  this.lanes = NoLanes;             // Work priority for this fiber
  this.childLanes = NoLanes;        // Work priority for children

  // Double buffering
  this.alternate = null;            // Current ↔ Work-in-progress pair
}

Key Architectural Concepts

1. Double Buffering (Current vs Work-in-Progress)

React maintains two fiber trees:

  • Current tree: Represents the current UI
  • Work-in-progress tree: Represents the next UI state being built

The alternate property links corresponding fibers between these trees. This allows React to build updates without affecting the current UI, then swap them atomically.

2. Linked List Structure

Instead of a traditional tree with children arrays, Fiber uses a linked list approach:

  • child: Points to first child
  • sibling: Points to next sibling
  • return: Points to parent

This structure allows React to pause and resume traversal efficiently.

3. Priority Lanes System

The lanes and childLanes properties implement React's priority system:

  • Different types of updates get different priorities
  • High-priority updates (user input) can interrupt low-priority updates (data fetching)
  • Multiple lanes can be active simultaneously

4. Work Loop

From packages/react-reconciler/src/ReactFiberWorkLoop.js:

function workLoopSync() {
  // Perform work without checking if we need to yield
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  // Perform work until we need to yield to the browser
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

The work loop processes fibers one by one. In concurrent mode, it checks shouldYield() to give control back to the browser for high-priority tasks.

Fiber Workflow

  1. Begin Phase: Process fiber, create children
  2. Complete Phase: Complete work, bubble up side effects
  3. Commit Phase: Apply changes to DOM (cannot be interrupted)

Benefits of Fiber Architecture

  • Incremental Rendering: Split work into chunks
  • Pause/Resume/Abort: Can stop work and come back later
  • Priority Assignment: Different updates get different priorities
  • Concurrency: Can work on multiple state versions
  • Better Error Boundaries: Can catch and handle errors better

2. Concurrent Mode

Overview

Concurrent Mode (now called Concurrent Features) enables React to interrupt rendering work to handle high-priority updates, making apps more responsive.

Core Concepts

1. Interruptible Rendering

Unlike synchronous rendering that blocks until complete, concurrent rendering can:

  • Pause work to handle higher-priority updates
  • Resume work later
  • Abandon work if no longer needed

2. Time Slicing

React breaks rendering work into small units and yields control back to the browser between units, preventing UI blocking.

3. Priority Levels

From the React source, different types of updates have different priorities:

// Simplified priority levels
const SyncLane = 0b00000000000000000000000000000001;        // Highest priority
const InputContinuousLane = 0b00000000000000000000000000100; // User interactions
const DefaultLane = 0b00000000000000000000000010000;        // Normal updates
const TransitionLane = 0b00000000000000000000010000000;     // Transitions
const IdleLane = 0b00100000000000000000000000000000;        // Lowest priority

Concurrent Mode Features

1. Automatic Batching

React 18+ automatically batches state updates in:

  • Event handlers (React 17 did this too)
  • Promises
  • setTimeout
  • Native event handlers
// All these updates are batched automatically
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // Only one re-render!
}, 1000);

2. Suspense Integration

Concurrent Mode works seamlessly with Suspense to show fallback UI while components load:

<Suspense fallback={<Spinner />}>
  <LazyComponent />
</Suspense>

3. Progressive Hydration

In SSR scenarios, concurrent mode enables:

  • Streaming HTML
  • Selective hydration (hydrate visible content first)
  • Responding to user input before full hydration

Implementation Details

From ReactFiberWorkLoop.js, the concurrent work loop checks if it should yield:

// Concurrent rendering can be interrupted
do {
  try {
    workLoopConcurrent();
    break;
  } catch (thrownValue) {
    handleThrow(root, thrownValue);
  }
} while (true);

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

The shouldYield() function checks:

  • Time elapsed since rendering started
  • Whether there are pending high-priority tasks
  • Browser needs (painting, user input)

Benefits

  • Improved Responsiveness: UI remains responsive during heavy rendering
  • Better Perceived Performance: Show partial results faster
  • Smoother Animations: Less jank during updates
  • Better Resource Utilization: CPU shares time between rendering and browser tasks

3. Suspense for Data Fetching

Overview

Suspense allows components to "wait" for something before rendering, by throwing a promise that React catches and handles gracefully.

How It Works

The Mechanism

When a component suspends, it throws a promise. React:

  1. Catches the thrown promise
  2. Shows the nearest Suspense boundary's fallback
  3. Waits for the promise to resolve
  4. Re-renders the component

From React test files:

function AsyncComponent() {
  if (!dataLoaded) {
    // Throw a promise to suspend
    throw fetchData().then(() => {
      dataLoaded = true;
    });
  }
  return <div>{data}</div>;
}

// Usage
<Suspense fallback={<Loading />}>
  <AsyncComponent />
</Suspense>

Implementation Pattern

// Cache to track promise states
const cache = new Map();

function fetchUser(userId) {
  if (!cache.has(userId)) {
    // Create promise and throw it
    const promise = fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        cache.set(userId, { status: 'success', data });
      })
      .catch(error => {
        cache.set(userId, { status: 'error', error });
      });
    
    cache.set(userId, { status: 'pending', promise });
    throw promise;
  }

  const cached = cache.get(userId);
  
  if (cached.status === 'pending') {
    throw cached.promise;
  }
  
  if (cached.status === 'error') {
    throw cached.error;
  }
  
  return cached.data;
}

function UserProfile({ userId }) {
  const user = fetchUser(userId); // May suspend
  return <div>{user.name}</div>;
}

Suspense Boundaries

From React source (ReactFiberThrow.js), when a component suspends:

// React catches the thrown promise and:
// 1. Marks the boundary with ShouldCapture flag
// 2. Enters unwind phase to find nearest Suspense boundary
// 3. Shows fallback UI
// 4. Attaches promise handlers to retry when ready

Nested Suspense

<Suspense fallback={<PageSkeleton />}>
  <MainContent />
  <Suspense fallback={<SidebarSkeleton />}>
    <Sidebar />
  </Suspense>
</Suspense>

Each Suspense boundary independently handles suspension in its subtree.

Suspense with Concurrent Mode

In concurrent mode, Suspense becomes even more powerful:

<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>
  • React can keep showing old UI while preparing new UI
  • Can abandon suspended trees if user navigates away
  • Supports progressive reveal of content

Best Practices

  1. Place Suspense boundaries strategically: Not too high (too much flicker), not too low (too many loading states)

  2. Use SuspenseList for coordinated loading (experimental):

<SuspenseList revealOrder="forwards">
  <Suspense fallback={<Skeleton />}><Post id={1} /></Suspense>
  <Suspense fallback={<Skeleton />}><Post id={2} /></Suspense>
  <Suspense fallback={<Skeleton />}><Post id={3} /></Suspense>
</SuspenseList>
  1. Combine with Error Boundaries: Suspense handles async loading, Error Boundaries handle failures

4. Passive Effects vs Layout Effects

Overview

React provides two types of effect hooks: useEffect (passive) and useLayoutEffect (layout). Understanding when each fires is crucial for correct behavior.

Implementation Details

From packages/react-reconciler/src/ReactFiberHooks.js:

// useEffect (Passive)
function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect,  // Passive flag
    HookPassive,
    create,
    deps,
  );
}

// useLayoutEffect (Layout)
function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect,
    HookLayout,  // Layout flag
    create,
    deps,
  );
}

Key Differences

Aspect useEffect (Passive) useLayoutEffect (Layout)
Timing After paint (async) After DOM mutations, before paint (sync)
Blocks Rendering No Yes
Use Cases Data fetching, subscriptions, logging DOM measurements, synchronous DOM updates
Performance Better (non-blocking) Can cause jank if slow

Execution Order

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

  useLayoutEffect(() => {
    console.log('1. Layout effect - before paint');
    return () => console.log('3. Layout cleanup');
  });

  useEffect(() => {
    console.log('2. Passive effect - after paint');
    return () => console.log('4. Passive cleanup');
  });

  return <div>{count}</div>;
}

Render sequence:

  1. React updates DOM
  2. Browser measures layout (doesn't paint yet)
  3. useLayoutEffect runs (synchronous)
  4. Browser paints
  5. useEffect runs (async)

When to Use Each

Use useEffect (default choice) for:

// ✅ Data fetching
useEffect(() => {
  fetchData().then(setData);
}, []);

// ✅ Event listeners
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

// ✅ Logging/analytics
useEffect(() => {
  trackPageView(pathname);
}, [pathname]);

Use useLayoutEffect for:

// ✅ Reading layout (avoid flicker)
useLayoutEffect(() => {
  const rect = elementRef.current.getBoundingClientRect();
  setHeight(rect.height);
}, []);

// ✅ Synchronous DOM updates before paint
useLayoutEffect(() => {
  // Prevent scroll jump
  if (scrollRef.current) {
    scrollRef.current.scrollTop = savedScrollPosition;
  }
}, []);

// ✅ Animations that need layout info
useLayoutEffect(() => {
  const start = element.getBoundingClientRect();
  // Update state
  element.animate([
    { transform: `translateY(${start.top}px)` },
    { transform: 'translateY(0)' }
  ], { duration: 300 });
}, [items]);

Visual Flicker Example

// ❌ BAD: Using useEffect causes flicker
function Tooltip() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    // Tooltip renders at (0,0), then jumps
    const rect = targetRef.current.getBoundingClientRect();
    setPosition({ x: rect.left, y: rect.bottom });
  }, []);
  
  return <div style={{ left: position.x, top: position.y }}>Tooltip</div>;
}

// ✅ GOOD: useLayoutEffect prevents flicker
function Tooltip() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useLayoutEffect(() => {
    // Position calculated before paint, no flicker
    const rect = targetRef.current.getBoundingClientRect();
    setPosition({ x: rect.left, y: rect.bottom });
  }, []);
  
  return <div style={{ left: position.x, top: position.y }}>Tooltip</div>;
}

SSR Considerations

useLayoutEffect doesn't run on the server (no DOM), which causes warnings:

// Suppress SSR warning when intentional
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

function Component() {
  useIsomorphicLayoutEffect(() => {
    // Safe for both SSR and client
  }, []);
}

5. Hydration

Overview

Hydration is the process of attaching React's event handlers and state management to server-rendered HTML, making it interactive.

How Hydration Works

From packages/react-reconciler/src/ReactFiberHydrationContext.js:

export const HydrationMismatchException: mixed = new Error(
  'Hydration Mismatch Exception: This is not a real error, and should not leak into ' +
  "userspace. If you're seeing this, it's likely a bug in React.",
);

The Hydration Process

  1. Server Renders HTML
// Server (Node.js)
import { renderToString } from 'react-dom/server';

const html = renderToString(<App />);
// Returns: '<div>Hello World</div>'
  1. HTML Sent to Client
<!DOCTYPE html>
<html>
  <body>
    <div id="root"><div>Hello World</div></div>
    <script src="bundle.js"></script>
  </body>
</html>
  1. Client Hydrates
// Client
import { hydrateRoot } from 'react-dom/client';

hydrateRoot(document.getElementById('root'), <App />);
// React attaches to existing DOM, adds event handlers

Hydration Flow

From ReactFiberBeginWork.js:

if (current === null) {
  // Initial mount
  
  // Special path for hydration
  // If we're currently hydrating, try to hydrate this boundary.
  if (getIsHydrating()) {
    // Try to match existing DOM nodes
    // If mismatch, fall back to client rendering
  }
}

Hydration Mismatches

Common causes of hydration errors:

// ❌ BAD: Different content on server vs client
function Clock() {
  return <div>{new Date().toISOString()}</div>;
  // Server time !== Client time → Mismatch!
}

// ✅ GOOD: Use effect for client-only content
function Clock() {
  const [time, setTime] = useState(null);
  
  useEffect(() => {
    setTime(new Date().toISOString());
  }, []);
  
  return <div>{time ?? 'Loading...'}</div>;
}

// ❌ BAD: Conditional rendering based on client-only APIs
function Component() {
  return (
    <div>
      {typeof window !== 'undefined' && <ClientOnly />}
    </div>
  );
}

// ✅ GOOD: Suppress hydration warning for client-only content
function Component() {
  const [mounted, setMounted] = useState(false);
  
  useEffect(() => {
    setMounted(true);
  }, []);
  
  return (
    <div>
      {mounted && <ClientOnly />}
    </div>
  );
}

React 18: Selective Hydration

React 18 introduces progressive/selective hydration with Concurrent Mode:

import { Suspense } from 'react';
import { hydrateRoot } from 'react-dom/client';

function App() {
  return (
    <Layout>
      <NavBar />
      <Suspense fallback={<Spinner />}>
        <Sidebar />
      </Suspense>
      <MainContent />
      <Suspense fallback={<Spinner />}>
        <Comments />
      </Suspense>
    </Layout>
  );
}

hydrateRoot(document.getElementById('root'), <App />);

Benefits:

  1. Streaming HTML: Server can send HTML before all components ready
  2. Selective Hydration: High-priority sections hydrate first
  3. Interrupt Hydration: User interactions interrupt low-priority hydration

Hydration Workflow

Server:
  1. renderToString(<App />) → HTML string
  2. Send HTML to client
  
Client:
  1. Parse HTML, display content (fast!)
  2. Download JavaScript bundle
  3. Execute React code
  4. hydrateRoot():
     - Walk existing DOM tree
     - Create Fiber tree
     - Match React elements to DOM nodes
     - Attach event listeners
     - Initialize state
  5. App is now interactive!

Best Practices

  1. Ensure Server/Client Consistency
// ✅ Use suppressHydrationWarning for intentional mismatches
<div suppressHydrationWarning>
  {typeof window !== 'undefined' ? 'Client' : 'Server'}
</div>
  1. Use Suspense for Code-Splitting
const LazyComponent = lazy(() => import('./Heavy'));

<Suspense fallback={<Skeleton />}>
  <LazyComponent />
</Suspense>
  1. Optimize Critical Path
// Inline critical CSS
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />

// Defer non-critical JS
<script defer src="non-critical.js"></script>

6. useTransition

Overview

useTransition allows you to mark state updates as non-urgent "transitions", keeping the UI responsive during expensive operations.

API Signature

From packages/react/src/ReactHooks.js:

export function useTransition(): [
  boolean,  // isPending flag
  (callback: () => void, options?: StartTransitionOptions) => void,  // startTransition
] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useTransition();
}

Basic Usage

import { useTransition, useState } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    // Urgent: Update input immediately
    setQuery(e.target.value);
    
    // Non-urgent: Update results (can be interrupted)
    startTransition(() => {
      const filtered = hugeList.filter(item => 
        item.includes(e.target.value)
      );
      setResults(filtered);
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <Results data={results} />
    </div>
  );
}

How It Works

  1. Priority System: Updates inside startTransition get lower priority
  2. Interruptible: Can be interrupted by urgent updates
  3. No Flicker: Old UI stays visible until new UI ready
  4. isPending: Indicates transition is in progress

Use Cases

1. Expensive List Filtering

function FilterableList({ items }) {
  const [filter, setFilter] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();

  const handleFilterChange = (value) => {
    setFilter(value); // Immediate
    
    startTransition(() => {
      // Expensive filtering (50,000 items)
      setFilteredItems(
        items.filter(item => 
          item.name.toLowerCase().includes(value.toLowerCase())
        )
      );
    });
  };

  return (
    <div>
      <input 
        value={filter}
        onChange={(e) => handleFilterChange(e.target.value)}
        disabled={isPending}
      />
      {isPending ? <Spinner /> : <List items={filteredItems} />}
    </div>
  );
}

2. Tab Switching

function Tabs() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const selectTab = (nextTab) => {
    startTransition(() => {
      setTab(nextTab);
    });
  };

  return (
    <div>
      <button onClick={() => selectTab('home')}>Home</button>
      <button onClick={() => selectTab('posts')}>Posts</button>
      <button onClick={() => selectTab('about')}>About</button>
      
      {isPending && <LoadingBar />}
      
      {tab === 'home' && <HomePage />}
      {tab === 'posts' && <PostsPage />}  {/* Heavy */}
      {tab === 'about' && <AboutPage />}
    </div>
  );
}

3. Route Transitions

function Router() {
  const [page, setPage] = useState('home');
  const [isPending, startTransition] = useTransition();

  const navigate = (url) => {
    startTransition(() => {
      setPage(url);
    });
  };

  return (
    <div>
      <nav style={{ opacity: isPending ? 0.5 : 1 }}>
        <a onClick={() => navigate('home')}>Home</a>
        <a onClick={() => navigate('profile')}>Profile</a>
      </nav>
      
      {page === 'home' && <HomePage />}
      {page === 'profile' && <ProfilePage />}
    </div>
  );
}

startTransition (Standalone)

You can also use startTransition without the isPending flag:

import { startTransition } from 'react';

// No hook needed
function handleClick() {
  startTransition(() => {
    setPage('/about');
  });
}

Comparing with Other Approaches

// ❌ Without useTransition: UI freezes during filter
const handleChange = (value) => {
  setQuery(value);
  setResults(expensiveFilter(items, value)); // Blocks UI!
};

// ⚠️ With debounce: Artificial delay
const handleChange = debounce((value) => {
  setQuery(value);
  setResults(expensiveFilter(items, value));
}, 300); // Always waits 300ms

// ✅ With useTransition: Responsive + fast when possible
const handleChange = (value) => {
  setQuery(value); // Immediate
  startTransition(() => {
    setResults(expensiveFilter(items, value)); // Smart priority
  });
};

Advanced: Transition Options (React 19+)

startTransition(() => {
  setPage('/about');
}, {
  // Custom options (future API)
  timeoutMs: 5000,
});

Best Practices

  1. Mark Long-Running Updates: Use for updates that take >16ms
  2. Keep Input Responsive: Never wrap controlled input updates
  3. Show Pending State: Use isPending for loading indicators
  4. Combine with Suspense: Great for lazy-loaded content

7. FlushSync and Deferred Updates

Overview

React provides APIs to control update scheduling: flushSync for urgent updates and deferred updates for low-priority work.

flushSync

Forces React to synchronously flush updates inside the callback.

API

import { flushSync } from 'react-dom';

flushSync(() => {
  // Updates here are applied immediately
  setState(newValue);
});
// DOM is updated here

When to Use flushSync

From React source comments in ReactFiberWorkLoop.js:

// For discrete and "default" updates (anything that's not flushSync),
// we want to wait for the microtasks to flush before unwinding.

Use Case 1: Measuring DOM After Update

function Component() {
  const [items, setItems] = useState([1, 2, 3]);
  const listRef = useRef();

  const addItem = () => {
    flushSync(() => {
      setItems([...items, items.length + 1]);
    });
    
    // DOM is updated immediately, can measure
    const height = listRef.current.scrollHeight;
    console.log('New height:', height);
  };

  return (
    <div ref={listRef}>
      {items.map(i => <div key={i}>Item {i}</div>)}
      <button onClick={addItem}>Add</button>
    </div>
  );
}

Use Case 2: Third-Party Library Integration

function Editor() {
  const [content, setContent] = useState('');
  const editorRef = useRef();

  const saveSelection = () => {
    // Need to ensure DOM is updated before calling library
    flushSync(() => {
      setContent(editorRef.current.innerHTML);
    });
    
    // Third-party library expects updated DOM
    externalLib.saveSelection(editorRef.current);
  };

  return <div ref={editorRef} contentEditable />;
}

Use Case 3: Focus Management

function TodoList() {
  const [todos, setTodos] = useState([]);
  const newTodoRef = useRef();

  const addTodo = (text) => {
    flushSync(() => {
      setTodos([...todos, { id: Date.now(), text }]);
    });
    
    // Focus new input immediately after it's in DOM
    newTodoRef.current?.focus();
  };

  return (
    <div>
      {todos.map(todo => <TodoItem key={todo.id} {...todo} />)}
      <input ref={newTodoRef} />
    </div>
  );
}

⚠️ Warning: Use flushSync Sparingly

// ❌ BAD: Overusing flushSync kills performance
function BadExample() {
  const handleClick = () => {
    flushSync(() => setState1(a));  // Render 1
    flushSync(() => setState2(b));  // Render 2
    flushSync(() => setState3(c));  // Render 3
    // Three separate renders! Very slow!
  };
}

// ✅ GOOD: Let React batch automatically
function GoodExample() {
  const handleClick = () => {
    setState1(a);
    setState2(b);
    setState3(c);
    // One batched render!
  };
}

Deferred Updates (useDeferredValue)

Complementary to useTransition, useDeferredValue lets you defer updating a value.

API

function useDeferredValue<T>(value: T, initialValue?: T): T

Basic Usage

function SearchResults({ query }) {
  // Defer the query value
  const deferredQuery = useDeferredValue(query);
  
  // Search with deferred query (can lag behind)
  const results = useSearch(deferredQuery);

  return (
    <div>
      {/* Show current query */}
      <p>Searching for: {query}</p>
      
      {/* Results may lag behind */}
      <Results 
        data={results}
        isStale={query !== deferredQuery}
      />
    </div>
  );
}

Deferred vs Transition

// With useTransition: You control when transition starts
function FilteredList() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  
  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    
    startTransition(() => {
      // You decide what to defer
      performExpensiveOperation(value);
    });
  };
}

// With useDeferredValue: React controls the deferral
function FilteredList() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  const handleChange = (e) => {
    setQuery(e.target.value);
    // React automatically defers deferredQuery
  };
  
  // Use deferred value for expensive work
  const results = useMemo(() => 
    expensiveFilter(items, deferredQuery),
    [deferredQuery]
  );
}

Practical Example: Search with Deferred Results

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  const results = useMemo(() => {
    return hugeList.filter(item =>
      item.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [deferredQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        {results.length === 0 ? (
          <p>No results</p>
        ) : (
          <ul>
            {results.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

Update Priorities Summary

From the CHANGELOG and source code:

Synchronous (Highest Priority)
  ↑
  ├─ flushSync() - Immediate, synchronous
  │
Urgent (High Priority)
  ├─ User input (typing, clicking)
  ├─ Discrete events
  │
Default (Normal Priority)
  ├─ Regular setState calls
  ├─ Network responses
  │
Transition (Low Priority)
  ├─ startTransition()
  ├─ useDeferredValue()
  │
Idle (Lowest Priority)
  ↓

8. Error Boundaries

Overview

Error Boundaries are React components that catch JavaScript errors in their child component tree, log them, and display fallback UI.

Implementation

From React tests and examples:

class ErrorBoundary extends React.Component {
  state = { error: null, errorInfo: null };

  static getDerivedStateFromError(error) {
    // Update state so next render shows fallback UI
    return { error };
  }

  componentDidCatch(error, errorInfo) {
    // Log error to error reporting service
    console.error('Error caught:', error);
    console.error('Component stack:', errorInfo.componentStack);
    
    this.setState({ error, errorInfo });
    
    // Send to error tracking service
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.error) {
      // Fallback UI
      return (
        <div>
          <h2>Something went wrong</h2>
          <details>
            <summary>Error details</summary>
            <pre>{this.state.error.toString()}</pre>
            <pre>{this.state.errorInfo.componentStack}</pre>
          </details>
          <button onClick={() => this.setState({ error: null })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

Usage

function App() {
  return (
    <ErrorBoundary>
      <Navigation />
      <ErrorBoundary>
        <Sidebar />
      </ErrorBoundary>
      <ErrorBoundary>
        <MainContent />
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

What Error Boundaries Catch

Caught:

  • Errors during rendering
  • Errors in lifecycle methods
  • Errors in constructors of child components

Not Caught:

  • Event handlers (use try/catch)
  • Asynchronous code (setTimeout, promises)
  • Server-side rendering
  • Errors in the error boundary itself

Event Handler Error Handling

// ❌ Error boundary won't catch this
function Button() {
  const handleClick = () => {
    throw new Error('Click error');
  };
  return <button onClick={handleClick}>Click</button>;
}

// ✅ Handle event errors manually
function Button() {
  const [error, setError] = useState(null);

  const handleClick = () => {
    try {
      riskyOperation();
    } catch (err) {
      setError(err);
      logError(err);
    }
  };

  if (error) return <ErrorDisplay error={error} />;
  
  return <button onClick={handleClick}>Click</button>;
}

Advanced Error Boundaries

1. Multiple Error Boundaries

function App() {
  return (
    <ErrorBoundary fallback={<PageError />}>
      <Header />
      
      <ErrorBoundary fallback={<SidebarError />}>
        <Sidebar />
      </ErrorBoundary>
      
      <ErrorBoundary fallback={<ContentError />}>
        <MainContent />
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

2. Error Boundary with Reset

class ErrorBoundary extends React.Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };
  }

  componentDidCatch(error, errorInfo) {
    logErrorToService(error, errorInfo);
  }

  reset = () => {
    this.setState({ error: null });
  };

  render() {
    if (this.state.error) {
      return this.props.fallback({ 
        error: this.state.error,
        reset: this.reset 
      });
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary fallback={({ error, reset }) => (
  <div>
    <h2>Error: {error.message}</h2>
    <button onClick={reset}>Try again</button>
  </div>
)}>
  <App />
</ErrorBoundary>

3. Error Boundary with Key-Based Reset

function App() {
  const [errorResetKey, setErrorResetKey] = useState(0);

  return (
    <ErrorBoundary 
      key={errorResetKey}
      fallback={
        <div>
          <h2>Error occurred</h2>
          <button onClick={() => setErrorResetKey(k => k + 1)}>
            Reload
          </button>
        </div>
      }
    >
      <MainApp />
    </ErrorBoundary>
  );
}

React Error Boundary Library

For a production-ready solution, use react-error-boundary:

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, errorInfo) => {
        logErrorToService(error, errorInfo);
      }}
      onReset={() => {
        // Reset app state
      }}
    >
      <MyApp />
    </ErrorBoundary>
  );
}

9. React.memo vs useMemo vs useCallback

Overview

React provides three memoization tools for performance optimization. Each serves a different purpose.

React.memo

Purpose: Memoize entire component to prevent re-renders when props haven't changed.

// Without memo: Re-renders on every parent render
function ExpensiveChild({ data }) {
  console.log('Rendering ExpensiveChild');
  return <div>{processData(data)}</div>;
}

// With memo: Only re-renders when data changes
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
  console.log('Rendering ExpensiveChild');
  return <div>{processData(data)}</div>;
});

// Custom comparison
const ExpensiveChild = React.memo(
  function ExpensiveChild({ data }) {
    return <div>{processData(data)}</div>;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip render)
    return prevProps.data.id === nextProps.data.id;
  }
);

useMemo

Purpose: Memoize expensive computation results.

From packages/react-reconciler/src/ReactFiberHooks.js:

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();  // Compute value
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];  // Return cached value
    }
  }
  
  const nextValue = nextCreate();  // Recompute
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Usage

function ProductList({ products, filter }) {
  // ❌ Without useMemo: Expensive filter runs every render
  const filtered = products.filter(p => p.category === filter);

  // ✅ With useMemo: Filter only when products or filter change
  const filtered = useMemo(
    () => products.filter(p => p.category === filter),
    [products, filter]
  );

  return <List items={filtered} />;
}

useCallback

Purpose: Memoize function identity (prevents creating new function on each render).

From React source:

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];  // Return cached function
    }
  }
  
  hook.memoizedState = [callback, nextDeps];
  return callback;  // Return new function
}

Usage

function TodoList({ todos }) {
  // ❌ Without useCallback: New function every render
  // Child components using this will re-render unnecessarily
  const handleToggle = (id) => {
    toggleTodo(id);
  };

  // ✅ With useCallback: Same function reference unless deps change
  const handleToggle = useCallback((id) => {
    toggleTodo(id);
  }, []);

  return todos.map(todo => (
    <TodoItem 
      key={todo.id} 
      todo={todo}
      onToggle={handleToggle}  // Stable reference
    />
  ));
}

Comparison Table

Tool Memoizes Use Case Example
React.memo Component Prevent component re-renders const Child = memo(Component)
useMemo Value Cache expensive calculations const value = useMemo(() => calc(), [deps])
useCallback Function Stable function reference const fn = useCallback(() => {}, [deps])

Practical Examples

Example 1: Combining All Three

// Parent component
function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  // useMemo: Cache filtered todos
  const filteredTodos = useMemo(() => {
    console.log('Filtering todos...');
    return todos.filter(todo => {
      if (filter === 'active') return !todo.completed;
      if (filter === 'completed') return todo.completed;
      return true;
    });
  }, [todos, filter]);

  // useCallback: Stable function reference
  const toggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);

  const addTodo = useCallback((text) => {
    setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
  }, []);

  return (
    <div>
      <FilterButtons filter={filter} onChange={setFilter} />
      <AddTodo onAdd={addTodo} />
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
    </div>
  );
}

// Child component with React.memo
const TodoList = memo(function TodoList({ todos, onToggle }) {
  console.log('Rendering TodoList');
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={onToggle} />
      ))}
    </ul>
  );
});

const TodoItem = memo(function TodoItem({ todo, onToggle }) {
  console.log('Rendering TodoItem', todo.id);
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
    </li>
  );
});

Example 2: When NOT to Use

// ❌ BAD: Over-optimization
function Component() {
  // Premature optimization - simple calculation
  const sum = useMemo(() => a + b, [a, b]);
  
  // Unnecessary - handler has no dependencies
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
  
  return <div onClick={handleClick}>{sum}</div>;
}

// ✅ GOOD: Simple and readable
function Component() {
  const sum = a + b;  // Just calculate it
  
  const handleClick = () => {
    console.log('clicked');
  };
  
  return <div onClick={handleClick}>{sum}</div>;
}

useCallback vs useMemo for Functions

// These are equivalent:
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

const memoizedCallback = useMemo(() => {
  return () => doSomething(a, b);
}, [a, b]);

// useCallback is just syntactic sugar!

Best Practices

  1. Measure First: Use React DevTools Profiler to identify slow components
  2. Memo Components That:
    • Render often
    • Re-render with same props
    • Are expensive to render
  3. useMemo for:
    • Expensive calculations
    • Creating objects/arrays passed to memoized children
  4. useCallback for:
    • Functions passed to memoized children
    • Dependencies in other hooks

10. Context Re-renders

Overview

Understanding how React Context causes re-renders is crucial for building performant applications with global state.

How Context Works

const ThemeContext = React.createContext();

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Main />
      <Footer />
    </ThemeContext.Provider>
  );
}

function Header() {
  const { theme } = useContext(ThemeContext);
  return <header className={theme}>Header</header>;
}

The Re-render Problem

⚠️ Issue: Every component using useContext re-renders when context value changes, even if they don't use the changed part.

const UserContext = React.createContext();

function App() {
  const [user, setUser] = useState({ name: 'John', age: 30 });
  const [theme, setTheme] = useState('light');
  
  // ❌ New object every render!
  const value = { user, setUser, theme, setTheme };
  
  return (
    <UserContext.Provider value={value}>
      <Profile />  {/* Uses only user */}
      <Settings /> {/* Uses only theme */}
    </UserContext.Provider>
  );
}

function Profile() {
  const { user } = useContext(UserContext);
  // Re-renders when theme changes, even though it doesn't use theme!
  return <div>{user.name}</div>;
}

Solution 1: Split Contexts

const UserContext = React.createContext();
const ThemeContext = React.createContext();

function App() {
  const [user, setUser] = useState({ name: 'John', age: 30 });
  const [theme, setTheme] = useState('light');
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <Profile />  {/* Only subscribes to UserContext */}
        <Settings /> {/* Only subscribes to ThemeContext */}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

function Profile() {
  const { user } = useContext(UserContext);
  // ✅ Only re-renders when user changes
  return <div>{user.name}</div>;
}

function Settings() {
  const { theme, setTheme } = useContext(ThemeContext);
  // ✅ Only re-renders when theme changes
  return <button onClick={() => setTheme('dark')}>{theme}</button>;
}

Solution 2: Memoize Context Value

function App() {
  const [user, setUser] = useState({ name: 'John', age: 30 });
  
  // ✅ Value only changes when user or setUser changes
  const value = useMemo(() => ({ user, setUser }), [user]);
  
  return (
    <UserContext.Provider value={value}>
      <Profile />
    </UserContext.Provider>
  );
}

Solution 3: Separate Data and Updaters

const UserStateContext = React.createContext();
const UserDispatchContext = React.createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'John', age: 30 });
  
  return (
    <UserStateContext.Provider value={user}>
      <UserDispatchContext.Provider value={setUser}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
}

// Component that reads
function UserDisplay() {
  const user = useContext(UserStateContext);
  // Re-renders when user changes
  return <div>{user.name}</div>;
}

// Component that only updates (doesn't re-render on user change!)
function UserForm() {
  const setUser = useContext(UserDispatchContext);
  // ✅ Doesn't re-render when user changes!
  
  return (
    <button onClick={() => setUser({ name: 'Jane', age: 25 })}>
      Update User
    </button>
  );
}

Solution 4: Context Selector Pattern

// Custom hook for selective subscription
function useContextSelector(context, selector) {
  const value = useContext(context);
  const selectedValue = selector(value);
  
  // Only re-render if selected value changed
  const [, forceUpdate] = useReducer(x => x + 1, 0);
  const selectedRef = useRef(selectedValue);
  
  useLayoutEffect(() => {
    if (!Object.is(selectedRef.current, selectedValue)) {
      selectedRef.current = selectedValue;
      forceUpdate();
    }
  });
  
  return selectedValue;
}

// Usage
const AppContext = React.createContext();

function Profile() {
  // ✅ Only re-renders when user.name changes
  const userName = useContextSelector(
    AppContext,
    state => state.user.name
  );
  
  return <div>{userName}</div>;
}

Solution 5: Use External State Management

For complex state, consider libraries designed for performance:

// Using Zustand
import create from 'zustand';

const useStore = create((set) => ({
  user: { name: 'John', age: 30 },
  theme: 'light',
  setUser: (user) => set({ user }),
  setTheme: (theme) => set({ theme }),
}));

function Profile() {
  // ✅ Only re-renders when user changes
  const user = useStore(state => state.user);
  return <div>{user.name}</div>;
}

function ThemeToggle() {
  // ✅ Only re-renders when theme changes
  const theme = useStore(state => state.theme);
  const setTheme = useStore(state => state.setTheme);
  return <button onClick={() => setTheme('dark')}>{theme}</button>;
}

Advanced Pattern: Bail Out with useMemo

const UserContext = React.createContext();

function App() {
  const [user, setUser] = useState({ name: 'John', age: 30 });
  const value = useMemo(() => ({ user, setUser }), [user]);
  
  return (
    <UserContext.Provider value={value}>
      <ExpensiveTree />
    </UserContext.Provider>
  );
}

// Wrap expensive tree in memo to bail out
const ExpensiveTree = memo(function ExpensiveTree() {
  // Even if parent re-renders, this tree won't re-render
  // unless it uses context or receives different props
  return (
    <div>
      <ChildA />
      <ChildB />
      <ChildC />
    </div>
  );
});

Context Re-render Rules

  1. All consumers re-render when provider value changes
  2. Comparison is by reference (uses Object.is)
  3. Memo doesn't help consumers - they always re-render
  4. Intermediate components can bail out with memo
const Context = React.createContext();

function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <Context.Provider value={count}>
      <Intermediate />
    </Context.Provider>
  );
}

// ✅ Intermediate doesn't re-render (no context usage, has memo)
const Intermediate = memo(() => {
  console.log('Intermediate render');
  return <Consumer />;
});

// ⚠️ Consumer always re-renders when context changes
function Consumer() {
  const count = useContext(Context);
  console.log('Consumer render');
  return <div>{count}</div>;
}

Performance Monitoring

import { Profiler } from 'react';

function App() {
  const onRenderCallback = (
    id,
    phase,
    actualDuration,
    baseDuration,
    startTime,
    commitTime
  ) => {
    console.log(`${id} ${phase} took ${actualDuration}ms`);
  };

  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <UserContext.Provider value={value}>
        <YourApp />
      </UserContext.Provider>
    </Profiler>
  );
}

Summary

This research covered 10 advanced React concepts with implementation details from the React source code:

  1. Fiber Architecture: React's reconciliation engine using linked lists and double buffering
  2. Concurrent Mode: Interruptible rendering with priority-based scheduling
  3. Suspense: Throwing promises for async data loading
  4. Passive vs Layout Effects: Understanding useEffect vs useLayoutEffect timing
  5. Hydration: Attaching React to server-rendered HTML
  6. useTransition: Marking updates as non-urgent for better UX
  7. flushSync & Deferred Updates: Controlling update scheduling
  8. Error Boundaries: Catching errors in component trees
  9. Memoization: React.memo, useMemo, and useCallback
  10. Context Re-renders: Optimizing context performance

All concepts are based on actual React source code from the facebook/react repository.


Repository: facebook/react (⭐ 240,440) Research Date: November 9, 2025 React Version: Main branch (React 19 development)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment