This document provides an in-depth analysis of advanced React concepts based on research from the official React repository (facebook/react).
- Fiber Architecture
- Concurrent Mode
- Suspense for Data Fetching
- Passive Effects vs Layout Effects
- Hydration
- useTransition
- FlushSync and Deferred Updates
- Error Boundaries
- React.memo vs useMemo vs useCallback
- Context Re-renders
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.
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
}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.
Instead of a traditional tree with children arrays, Fiber uses a linked list approach:
child: Points to first childsibling: Points to next siblingreturn: Points to parent
This structure allows React to pause and resume traversal efficiently.
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
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.
- Begin Phase: Process fiber, create children
- Complete Phase: Complete work, bubble up side effects
- Commit Phase: Apply changes to DOM (cannot be interrupted)
- 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
Concurrent Mode (now called Concurrent Features) enables React to interrupt rendering work to handle high-priority updates, making apps more responsive.
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
React breaks rendering work into small units and yields control back to the browser between units, preventing UI blocking.
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 priorityReact 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);Concurrent Mode works seamlessly with Suspense to show fallback UI while components load:
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>In SSR scenarios, concurrent mode enables:
- Streaming HTML
- Selective hydration (hydrate visible content first)
- Responding to user input before full hydration
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)
- 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
Suspense allows components to "wait" for something before rendering, by throwing a promise that React catches and handles gracefully.
When a component suspends, it throws a promise. React:
- Catches the thrown promise
- Shows the nearest Suspense boundary's fallback
- Waits for the promise to resolve
- 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>// 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>;
}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<Suspense fallback={<PageSkeleton />}>
<MainContent />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>Each Suspense boundary independently handles suspension in its subtree.
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
-
Place Suspense boundaries strategically: Not too high (too much flicker), not too low (too many loading states)
-
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>- Combine with Error Boundaries: Suspense handles async loading, Error Boundaries handle failures
React provides two types of effect hooks: useEffect (passive) and useLayoutEffect (layout). Understanding when each fires is crucial for correct behavior.
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,
);
}| 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 |
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:
- React updates DOM
- Browser measures layout (doesn't paint yet)
useLayoutEffectruns (synchronous)- Browser paints
useEffectruns (async)
// ✅ Data fetching
useEffect(() => {
fetchData().then(setData);
}, []);
// ✅ Event listeners
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// ✅ Logging/analytics
useEffect(() => {
trackPageView(pathname);
}, [pathname]);// ✅ 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]);// ❌ 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>;
}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
}, []);
}Hydration is the process of attaching React's event handlers and state management to server-rendered HTML, making it interactive.
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.",
);- Server Renders HTML
// Server (Node.js)
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
// Returns: '<div>Hello World</div>'- HTML Sent to Client
<!DOCTYPE html>
<html>
<body>
<div id="root"><div>Hello World</div></div>
<script src="bundle.js"></script>
</body>
</html>- Client Hydrates
// Client
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
// React attaches to existing DOM, adds event handlersFrom 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
}
}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 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:
- Streaming HTML: Server can send HTML before all components ready
- Selective Hydration: High-priority sections hydrate first
- Interrupt Hydration: User interactions interrupt low-priority hydration
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!
- Ensure Server/Client Consistency
// ✅ Use suppressHydrationWarning for intentional mismatches
<div suppressHydrationWarning>
{typeof window !== 'undefined' ? 'Client' : 'Server'}
</div>- Use Suspense for Code-Splitting
const LazyComponent = lazy(() => import('./Heavy'));
<Suspense fallback={<Skeleton />}>
<LazyComponent />
</Suspense>- Optimize Critical Path
// Inline critical CSS
<style dangerouslySetInnerHTML={{ __html: criticalCSS }} />
// Defer non-critical JS
<script defer src="non-critical.js"></script>useTransition allows you to mark state updates as non-urgent "transitions", keeping the UI responsive during expensive operations.
From packages/react/src/ReactHooks.js:
export function useTransition(): [
boolean, // isPending flag
(callback: () => void, options?: StartTransitionOptions) => void, // startTransition
] {
const dispatcher = resolveDispatcher();
return dispatcher.useTransition();
}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>
);
}- Priority System: Updates inside
startTransitionget lower priority - Interruptible: Can be interrupted by urgent updates
- No Flicker: Old UI stays visible until new UI ready
- isPending: Indicates transition is in progress
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>
);
}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>
);
}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>
);
}You can also use startTransition without the isPending flag:
import { startTransition } from 'react';
// No hook needed
function handleClick() {
startTransition(() => {
setPage('/about');
});
}// ❌ 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
});
};startTransition(() => {
setPage('/about');
}, {
// Custom options (future API)
timeoutMs: 5000,
});- Mark Long-Running Updates: Use for updates that take >16ms
- Keep Input Responsive: Never wrap controlled input updates
- Show Pending State: Use
isPendingfor loading indicators - Combine with Suspense: Great for lazy-loaded content
React provides APIs to control update scheduling: flushSync for urgent updates and deferred updates for low-priority work.
Forces React to synchronously flush updates inside the callback.
import { flushSync } from 'react-dom';
flushSync(() => {
// Updates here are applied immediately
setState(newValue);
});
// DOM is updated hereFrom 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.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>
);
}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 />;
}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>
);
}// ❌ 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!
};
}Complementary to useTransition, useDeferredValue lets you defer updating a value.
function useDeferredValue<T>(value: T, initialValue?: T): Tfunction 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>
);
}// 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]
);
}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>
);
}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)
↓
Error Boundaries are React components that catch JavaScript errors in their child component tree, log them, and display fallback UI.
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;
}
}function App() {
return (
<ErrorBoundary>
<Navigation />
<ErrorBoundary>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
);
}✅ 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
// ❌ 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>;
}function App() {
return (
<ErrorBoundary fallback={<PageError />}>
<Header />
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
);
}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>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>
);
}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>
);
}React provides three memoization tools for performance optimization. Each serves a different purpose.
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;
}
);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;
}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} />;
}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
}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
/>
));
}| 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]) |
// 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>
);
});// ❌ 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>;
}// 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!- Measure First: Use React DevTools Profiler to identify slow components
- Memo Components That:
- Render often
- Re-render with same props
- Are expensive to render
- useMemo for:
- Expensive calculations
- Creating objects/arrays passed to memoized children
- useCallback for:
- Functions passed to memoized children
- Dependencies in other hooks
Understanding how React Context causes re-renders is crucial for building performant applications with global state.
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>;
}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>;
}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>;
}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>
);
}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>
);
}// 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>;
}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>;
}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>
);
});- All consumers re-render when provider value changes
- Comparison is by reference (uses
Object.is) - Memo doesn't help consumers - they always re-render
- 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>;
}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>
);
}This research covered 10 advanced React concepts with implementation details from the React source code:
- Fiber Architecture: React's reconciliation engine using linked lists and double buffering
- Concurrent Mode: Interruptible rendering with priority-based scheduling
- Suspense: Throwing promises for async data loading
- Passive vs Layout Effects: Understanding useEffect vs useLayoutEffect timing
- Hydration: Attaching React to server-rendered HTML
- useTransition: Marking updates as non-urgent for better UX
- flushSync & Deferred Updates: Controlling update scheduling
- Error Boundaries: Catching errors in component trees
- Memoization: React.memo, useMemo, and useCallback
- 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)