The Scheduler, The Fiber, and The Reconciler: A Deep Dive into React’s Core


Most React developers are familiar with the concept of the Virtual DOM. We’re taught that when we call setState, React creates a new virtual tree, “diffs” it with the old one, and efficiently updates the actual browser DOM. While true, this high-level explanation barely scratches the surface of the sophisticated engine running under the hood. It doesn’t answer the critical questions: How does React handle multiple, competing updates? What allows it to render fluid animations while also fetching data or responding to user input without freezing the page? The simple diffing algorithm is only the beginning of the story.


The Evolution of React’s Reconciler

Introduction to Reconciliation

At the heart of every React application lies a powerful process known as reconciliation. This is the fundamental mechanism React uses to ensure that the user interface (UI) you see in the browser is always a precise reflection of the application’s current state. Whenever the state of your application changes—perhaps a user clicks a button, data arrives from a server, or an input field is updated—React initiates this reconciliation process to efficiently update the UI.

To understand how this works, we first need to grasp the concept of the Virtual DOM. Instead of directly manipulating the browser’s Document Object Model (DOM), which can be slow and resource-intensive, React maintains a lightweight, in-memory representation of it. This Virtual DOM is essentially a JavaScript object that mirrors the structure of the real DOM. Working with this JavaScript object is significantly faster than interacting with the actual browser DOM.

When a React component renders for the first time, React creates a complete Virtual DOM tree for that component and its children. Let’s consider a simple car rental application. We might have a CarListComponent that displays a list of available vehicles.

import React from 'react';

function CarListComponent({ cars }) {
return (
<div>
<h1>Available Cars</h1>
{cars.map(car => (
<div key={car.id} className="car-item">
<h2>{car.make} {car.model}</h2>
<p>Price per day: ${car.price}</p>
</div>
))}
</div>
);
}

When this component first renders, React builds a Virtual DOM tree that looks something like this (in a simplified view):

{
type: 'div',
props: {
children: [
{ type: 'h1', props: { children: 'Available Cars' } },
// ... and so on for each car
]
}
}

This entire structure exists only in JavaScript memory. React then takes this Virtual DOM and uses it to create the actual DOM elements that are displayed on the screen.

The magic happens when the state changes. Imagine the user applies a filter to see only sedans. This action updates the cars prop, triggering a re-render of CarListComponent. Now, React doesn’t just throw away the old UI and build a new one from scratch. Instead, it creates a new Virtual DOM tree based on the updated state.

With two versions of the Virtual DOM in memory—the previous one and the new one—React performs what is known as a “diffing” algorithm. It efficiently compares, or “diffs,” the new Virtual DOM against the old one to identify the exact, minimal set of changes required to bring the real DOM to the desired state. It walks through both trees, node by node, and compiles a list of mutations. For instance, it might determine that three div elements representing SUVs need to be removed and two new div elements for sedans need to be added.

Once this “diff” is calculated, React proceeds to the final step: it takes this list of changes and applies them to the real browser DOM in a single, optimised batch. This targeted approach is what makes React so performant. By limiting direct manipulation of the DOM to only what is absolutely necessary, it avoids costly reflows and repaints, resulting in a smooth and responsive user experience. This entire cycle—creating a new Virtual DOM on state change, diffing it with the old one, and updating the real DOM—is the essence of reconciliation.

The Stack Reconciler (Pre-Fiber)

Before the release of React 16, the engine driving the reconciliation process was what we now refer to as the Stack Reconciler. Its name comes from its reliance on the call stack to manage the rendering work. This version of the reconciler operated in a synchronous and recursive manner. When a state or prop update occurred, React would start at the root of the affected component tree and recursively traverse the entire structure, calculating the differences and applying them to the DOM.

The key characteristic of this approach was its uninterruptible nature. Once the reconciliation process began, it would continue until the entire component tree was processed and the call stack was empty. This all-or-nothing approach worked well for smaller applications, but its limitations became apparent as user interfaces grew in complexity.

Let’s return to our car rental application to see this in action. Imagine a more complex UI where users can not only see a list of cars but also apply multiple filters, sort the results, and view detailed specifications for each vehicle, all within a single, intricate component tree.

// A hypothetical complex component structure
function CarDashboard({ filters, sortBy }) {
const filteredCars = applyFilters(CARS_DATA, filters);
const sortedCars = applySorting(filteredCars, sortBy);

return (
<div>
<FilterControls />
<SortOptions />
<div className="car-grid">
{sortedCars.map(car => (
<CarCard key={car.id} car={car}>
<CarImage image={car.imageUrl} />
<CarSpecs specs={car.specifications} />
<BookingButton price={car.price} />
</CarCard>
))}
</div>
</div>
);
}

In this example, a single update to the filters prop of CarDashboard would trigger the Stack Reconciler. React would recursively call the render method (or functional component equivalent) for CarDashboard, then for every CarCard, and for every CarImage, CarSpecs, and BookingButton within them. This creates a deep call stack of functions that need to be executed.

The critical issue here is that all of this work happens synchronously on the main thread. The main thread is the single thread in a browser responsible for handling everything from executing JavaScript to responding to user input like clicks and scrolls, and performing layout and paint operations.

If our CarDashboard renders hundreds of cars with deeply nested components, the reconciliation process could take a significant amount of time—perhaps several hundred milliseconds. During this entire period, the main thread is completely blocked. It cannot do anything else. If a user tries to click a button or scroll the page while the Stack Reconciler is busy, the browser won’t be able to respond until the reconciliation is complete. This leads to a frozen or “janky” user interface, creating a poor user experience.

Consider an animation, like a loading spinner, that should be running while the new car list is being prepared. With the Stack Reconciler blocking the main thread, the JavaScript needed to update the animation’s frames cannot run. The result is a stuttering or completely frozen animation. This fundamental limitation—its inability to pause, defer, or break up the rendering work—was the primary motivation for the React team to completely rewrite the reconciler. It became clear that for modern, highly interactive applications, a new approach was needed that could yield to the browser and prioritize work more intelligently.

The Advent of the Fiber Reconciler

To overcome the inherent limitations of the synchronous Stack Reconciler, the React team embarked on a multi-year project to completely rewrite its core algorithm. The result, unveiled in React 16, is the Fiber Reconciler. This wasn’t just an update; it was a fundamental rethinking of how reconciliation should work, designed specifically for the complex and dynamic user interfaces of modern web applications.

The primary goal of the Fiber Reconciler is to enable incremental and asynchronous rendering. Unlike its predecessor, Fiber is designed to be interruptible. It can break down the rendering work into smaller, manageable chunks, and pause its work to yield control back to the browser’s main thread. This means that high-priority updates, like user input or critical animations, can be handled immediately, without having to wait for a large, time-consuming render to complete.

At its core, Fiber introduces a new data structure, also called a “fiber,” which represents a unit of work. Instead of a recursive traversal that fills the call stack, React now creates a linked list of these fiber objects. This new architecture allows React to walk through the component tree, process a few units of work, and then, if a higher-priority task appears or if it’s running out of its allotted time slice, it can pause the reconciliation process. Once the main thread is free again, React can pick up right where it left off.

Let’s revisit our complex car rental application to see the profound impact of this change.

// The same complex component from the previous section
function CarDashboard({ filters, sortBy }) {
// ... filtering and sorting logic ...

// A new component to show a typing indicator
const [isTyping, setIsTyping] = useState(false);

return (
<div>
<FilterControls onTypingChange={setIsTyping} />
<SortOptions />
{isTyping && <div className="typing-indicator">Filtering...</div>}
<div className="car-grid">
{/* ... mapping over sortedCars ... */}
</div>
</div>
);
}

Imagine a user is typing in a search box within the <FilterControls /> component. With the old Stack Reconciler, each keystroke would trigger a full, synchronous re-render of the entire car-grid. If rendering the grid takes 200ms, but the user is typing a new character every 100ms, the UI would feel sluggish and unresponsive. The typing-indicator might never even appear because the main thread would be perpetually blocked by the rendering work.

With the Fiber Reconciler, the outcome is dramatically different. As the user types, React begins the rendering work for the updated car-grid. However, it doesn’t do it all at once. It processes a few CarCard components, then yields to the main thread. This gives the browser a chance to process the next keystroke or render the typing-indicator. The reconciliation of the car-grid happens incrementally, in the background, without freezing the UI.

This ability to pause, resume, and prioritize work is the superpower of the Fiber Reconciler. It allows React to build fluid and responsive user experiences, even in applications with complex animations, demanding data visualizations, and intricate component hierarchies. It lays the groundwork for advanced features like Concurrent Mode, Suspense for data fetching, and improved server-side rendering, fundamentally changing what’s possible in a React application.

Deconstructing the Fiber Architecture

What is a Fiber?

At the heart of React’s modern reconciler is a plain JavaScript object called a fiber. It’s much more than just a data structure; a fiber represents a unit of work. Instead of thinking of rendering as a single, monolithic task, the Fiber architecture breaks down the rendering of a component tree into thousands of these discrete units. This allows React to start, pause, and resume rendering work, which is the key to enabling non-blocking, asynchronous rendering.

Every single component instance in your application, whether it’s a class component, a function component, or even a simple HTML tag like div, has a corresponding fiber object. Let’s examine the essential properties of a fiber object to understand how it orchestrates the rendering process, using our car rental application as a backdrop.

Imagine we have a CarCard component that receives new props. React will create a fiber object for it. While the actual fiber has many properties, we’ll focus on the most critical ones.

// A simplified representation of a CarCard component
function CarCard({ car }) {
return (
<div key={car.id} className="card">
<h3>{car.make} {car.model}</h3>
<p>Price: ${car.price}</p>
</div>
);
}

A fiber for this component would contain the following key properties:

  • type and key: These properties identify the component associated with the fiber. The type would point to the CarCard function itself. The key (in our case, car.id) is the unique identifier you provide in a list, which helps React efficiently track additions, removals, and re-orderings without having to re-render every item.
  • child, sibling and return pointers: This is where Fiber departs dramatically from the old Stack Reconciler. Instead of relying on recursive function calls to traverse the component tree, a fiber tree is a linked list. Each fiber has pointers to its first child, its next sibling, and its return (or parent) fiber. This flat, pointer-based structure allows React to traverse the tree without deep recursion, meaning it can stop at any point and know exactly how to resume later.
  • pendingProps and memoizedProps: These properties are crucial for determining if a component needs to re-render. memoizedProps holds the props that were used to render the component last time. pendingProps holds the new props that have just been passed down from the parent. During the reconciliation process, React compares pendingProps with memoizedProps. If they are different, the component needs to be updated. For our CarCard, if the car.price in pendingProps is different from the price in memoizedProps, React knows it must re-render this component.
  • alternate: This property is the linchpin of Fiber’s ability to perform work without affecting the visible UI. It implements a technique called double buffering. At any given time, there are two fiber trees: the current tree, which represents the UI currently on the screen, and the work-in-progress tree, which is where React builds updates off-screen. The alternate property of a fiber in the current tree points to its corresponding fiber in the work-in-progress tree, and vice-versa. When a state update occurs, React clones the affected fibers from the current tree to create the work-in-progress tree. All the diffing and rendering work happens on this off-screen tree. Once the work is complete, React atomically swaps the work-in-progress tree to become the new current tree. This process is seamless and prevents UI tearing or showing inconsistent states to the user.

By representing the entire application as a tree of these granular fiber objects, React gains incredible control over the rendering process. It’s no longer a black box that runs to completion. Instead, it’s a series of schedulable units of work that can be executed according to their priority, ensuring that the most critical updates are always handled first, leading to a fluid and responsive application.

How Fiber Enables Asynchronous Rendering

The true power of the Fiber architecture lies in how it uses the linked-list structure of the fiber tree to achieve asynchronous rendering. Because each fiber is a distinct unit of work with explicit pointers to its child, sibling, and return fibers, React is no longer forced into an uninterruptible, recursive traversal. Instead, it can walk the tree incrementally and, most importantly, pause at any time without losing its place.

This process is managed by a work loop. When a render is triggered, React starts at the root of the work-in-progress tree and begins traversing it according to a specific algorithm:

  1. Begin Work: React performs the work for the current fiber. This involves comparing its pendingProps to its memoizedProps to see if it needs to update.
  2. Move to Child: If the fiber has a child, React makes that child the next unit of work.
  3. Move to Sibling: If the fiber has no child, React moves to its sibling and makes that the next unit of work.
  4. Return: If the fiber has no child and no sibling, React moves up the tree using the return pointer until it finds a fiber with a sibling to work on, or until it completes the entire tree.

This predictable, manual traversal is the key. Between processing any two fibers, React can check if there’s more urgent work to do, such as responding to user input. If there is, it can simply pause the work loop, leaving the fiber tree in its current state, and yield to the main thread.

Let’s visualize this with our car rental application. Assume we have a list of 100 CarCard components to render after a filter is applied.

// A parent component that renders a list of CarCards
function CarList({ cars }) {
// A high-priority state update for user input
const [inputValue, setInputValue] = useState('');

return (
<div>
<input
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="Type to highlight a car..."
/>
<div className="grid">
{cars.map(car => <CarCard key={car.id} car={car} />)}
</div>
</div>
);
}

When the cars prop changes, React starts its work loop on the <div className=”grid”>. It processes the first CarCard fiber, then its sibling (the second CarCard), and so on. Now, imagine after processing the tenth CarCard, the user starts typing into the <input>.

The onChange event is a high-priority update. The Fiber reconciler, after completing work on the tenth CarCard, can detect this pending high-priority update. Instead of continuing to the eleventh CarCard, it pauses the low-priority rendering of the list. It records its progress—knowing the next unit of work is the eleventh CarCard—and yields control to the main thread.

The browser is now free to handle the input event, updating the inputValue state and re-rendering the input field. The user sees immediate feedback for their typing, and the UI remains fluid. Once the main thread is idle again, React resumes its previous work exactly where it left off, beginning its work loop on the eleventh CarCard fiber. This ability to pause, yield, and resume—or even abort the old work if new props come in—is what we call asynchronous rendering. It ensures that long rendering tasks don’t block the main thread, leading to a vastly superior and more responsive user experience.

The Role of the Scheduler

Prioritizing Updates

While the Fiber Reconciler provides the mechanism for pausing and resuming work, it doesn’t decide when that should happen. That crucial responsibility falls to another key part of React’s core: the Scheduler. The Scheduler acts as a sophisticated traffic controller for all pending state updates, organizing them into a prioritized queue. Its fundamental job is to tell the Reconciler which unit of work to perform next, ensuring that the most critical updates are processed first, leading to a fluid and responsive application.

To achieve this, the Scheduler assigns a priority level to every update. This allows React to differentiate between an urgent user interaction and a less critical background task. Let’s explore these priority levels within the context of our car rental application.

The highest priority is Synchronous. This level is reserved for updates that must be handled immediately and cannot be deferred. A primary example is updates to uncontrolled inputs. If a user is typing into a search box, they expect to see their characters appear instantly. React handles these updates synchronously to guarantee immediate feedback, as any delay would feel broken.

Next is what can be considered Task or User-Blocking priority. These are high-priority updates, typically initiated by direct user interaction, that should be completed quickly to avoid making the UI feel sluggish. For instance, when a user clicks a button to apply a “SUV” filter, they expect the list of cars to update promptly.

import { useState } from 'react';

function FilterComponent({ onFilterChange }) {
const handleFilterClick = () => {
// This setState call is treated as a high-priority, user-blocking update.
// The user has clicked something and expects a fast response.
onFilterChange('SUV');
};

return <button onClick={handleFilterClick}>Show SUVs</button>;
}

In this case, the Scheduler ensures that the work to re-render the car list begins almost immediately. It’s not strictly synchronous—it can still be broken up by the Fiber Reconciler—but it’s placed at the front of the queue, ahead of any lower-priority work.

A distinct level exists for Animation priority. This is for updates that need to complete within a single animation frame to create smooth visual effects, such as those managed by requestAnimationFrame. Imagine in our car rental app, clicking on a car card smoothly expands it to reveal more details. The state update that controls this expansion—for example, changing its height from 100px to 400px—would be scheduled with animation priority to prevent visual stuttering or “jank.”

Finally, there is Idle priority. This is the lowest priority level, reserved for background tasks or deferred work that can be performed whenever the browser is idle. This is perfect for non-essential tasks that don’t impact the current user experience. For example, we could pre-fetch data for a “You Might Also Like” section while the user is browsing the main car list.

import { useEffect } from 'react';

// A custom hook to pre-fetch data when the browser is idle
function useIdlePrefetch(url) {
useEffect(() => {
// The 'startTransition' API (or a similar internal mechanism)
// tells React to treat this state update as low-priority.
React.startTransition(() => {
// This fetch call and subsequent state update will only run
// when the main thread is not busy with higher-priority tasks.
fetch(url).then(res => res.json()).then(setData);
});
}, [url]);
}

By intelligently categorizing every update, the Scheduler provides the Reconciler with a clear order of operations. It ensures that a user’s click is always more important than a background data fetch, and that a smooth animation is never interrupted by a slow re-render, forming the foundation of a truly performant and user-centric application.

Yielding to the Main Thread

The Scheduler’s ability to prioritize updates would be of little use without a mechanism to act on those priorities. This is where the concept of yielding to the main thread becomes critical. The browser’s main thread is a single, precious resource responsible for executing JavaScript, handling user interactions, and painting pixels to the screen. If a single task, like rendering a large component tree, monopolizes this thread for too long, the entire application freezes. This is what users perceive as “jank” or unresponsiveness.

To prevent this, the Scheduler and the Fiber Reconciler work in close cooperation. The Scheduler doesn’t just tell the Reconciler what to do next; it also gives it a deadline. It essentially says, “Work on this task, but you must yield control back to me if a higher-priority task arrives or if you’ve been working for more than a few milliseconds (a time slice).” This cooperative scheduling ensures that no single rendering task can ever block the main thread for a significant period.

Let’s see how this plays out in our car rental application. Imagine we have a feature that renders a complex, data-heavy AnalyticsDashboard component. This is a low-priority update that we trigger in the background. At the same time, the user can click a “Quick Book” button for a featured car, which is a high-priority action.

function CarRentalApp() {
const [showDashboard, setShowDashboard] = useState(false);

// High-priority action: A user clicks to book a car
const handleQuickBook = () => {
// This is a high-priority update
alert('Car booked! Confirmation will be sent shortly.');
};

useEffect(() => {
// Low-priority action: We decide to render a heavy component
// in the background after the initial page load.
// React's 'startTransition' marks this as a non-urgent update.
React.startTransition(() => {
setShowDashboard(true);
});
}, []);

return (
<div>
<h1>Featured Car</h1>
<button onClick={handleQuickBook}>Quick Book Now</button>
<hr />
{/* The AnalyticsDashboard is a very large and slow component */}
{showDashboard && <AnalyticsDashboard />}
</div>
);
}

Here’s the sequence of events:

  1. Low-Priority Work Begins: After the initial render, the useEffect hook fires. The startTransition call tells the Scheduler that setting showDashboard to true is a low-priority update. The Scheduler instructs the Reconciler to start rendering the AnalyticsDashboard.
  2. Work in Progress: The Reconciler begins its work loop, processing the fibers for the AnalyticsDashboard one by one. This is a slow component, and the work will take, say, 300 milliseconds to complete.
  3. High-Priority Interruption: After 50 milliseconds of rendering the dashboard, the user clicks the “Quick Book Now” button. This onClick event is a high-priority task.
  4. The Scheduler Intervenes: The Scheduler immediately sees this new, high-priority update. It checks its clock and sees that the Reconciler has been working on the low-priority task. It signals to the Reconciler that it must yield.
  5. Reconciler Pauses: After finishing its current unit of work (the fiber it’s currently processing), the Reconciler pauses. It doesn’t throw away its progress on the AnalyticsDashboard; it simply leaves the work-in-progress tree in its partially completed state.
  6. Main Thread is Free: Control is returned to the main thread. The browser is now free to execute the handleQuickBook event handler. The alert appears instantly. The user gets immediate feedback.
  7. Work Resumes: Once the high-priority task is complete and the main thread is idle, the Scheduler tells the Reconciler it can resume its work on the AnalyticsDashboard right where it left off.

This act of yielding is the cornerstone of a responsive React application. It ensures that no matter how much work is happening in the background, the application is always ready to respond to the user’s most recent and important interactions.

The Two Phases of Rendering

The Render Phase (or “Reconciliation Phase”)

The first stage of React’s update process is the Render Phase. During this phase, React discovers what changes need to be made to the UI. Its goal is to create a new “work-in-progress” Fiber tree that represents the future state of your application. It’s crucial to understand that this phase is purely computational; it involves calling your components and comparing the results with the previous render, but it does not touch the actual browser DOM.

The most important characteristic of the Render Phase is that it is asynchronous and interruptible. Because React is only working with its internal fiber objects, it can perform this work in small chunks, pausing to yield to the main thread for more urgent tasks, or even discarding the work altogether if a newer, higher-priority update comes in. This is the magic that prevents UI blocking.

Several component lifecycle methods are executed during this phase. This is the point where React gives you, the developer, an opportunity to influence the rendering outcome. These methods include the constructor, getDerivedStateFromProps, shouldComponentUpdate, and, most famously, the render method itself.

Let’s consider a CarDetails class component in our application that displays information about a selected vehicle.

class CarDetails extends React.Component {
constructor(props) {
super(props);
// 1. constructor: Runs once. Good for initializing state.
this.state = { isFavorite: false };
}

static getDerivedStateFromProps(nextProps, prevState) {
// 2. getDerivedStateFromProps: Runs on every render.
// Use this to derive state from props over time.
// For example, resetting a view when the car ID changes.
return null; // Or return an object to update state
}

shouldComponentUpdate(nextProps, nextState) {
// 3. shouldComponentUpdate: Your chance to optimize.
// If the price hasn't changed, we can skip this entire update.
if (this.props.car.price === nextProps.car.price) {
return false; // Tells React to bail out of the render process for this component
}
return true;
}

render() {
// 4. render: The core of the phase. Purely describes what the UI should look like.
const { car } = this.props;
return (
<div>
<h1>{car.make} {car.model}</h1>
<p>Price: ${car.price}</p>
{/* ... other details ... */}
</div>
);
}
}

In older versions of React, this phase also included methods like componentWillMount, componentWillReceiveProps, and componentWillUpdate. These are now prefixed with UNSAFE_ because the interruptible nature of the Render Phase makes them dangerous for certain tasks, particularly side effects like making API calls.

Why are they considered unsafe? Imagine our application starts rendering an update to the CarDetails component because a new discount is being calculated. React calls UNSAFE_componentWillUpdate. Inside this method, we might have naively placed an API call to log this “view update” event.

// UNSAFE_componentWillUpdate(nextProps) {
// // DANGEROUS: This side effect is in the Render Phase.
// api.logEvent('user is viewing updated price', nextProps.car.id);
// }

Now, before this low-priority render can complete, the user clicks a button for a high-priority action. The Scheduler interrupts the CarDetails render, discards the work, and handles the user’s click. Later, React restarts the CarDetails render from scratch, and UNSAFE_componentWillUpdate is called a second time for the same logical update. Our logging service would now have two duplicate events. Worse, the first render could have been aborted entirely, meaning the method was called but the UI was never actually updated, leading to inconsistent analytics.

Because the Render Phase can be paused, restarted, or aborted, any code within it may be executed multiple times or not at all before a final decision is made. Therefore, this phase must be kept “pure”—free of side effects. Its sole responsibility is to describe the desired UI, leaving all mutations and side effects to the next, non-interruptible phase.

The Commit Phase

The Commit Phase is the second and final stage of React’s rendering process. This is where React takes the “work-in-progress” Fiber tree, which was calculated during the Render Phase, and applies the necessary changes to the actual browser DOM. Once this phase begins, it is synchronous and cannot be interrupted. This uninterruptible nature is crucial because it guarantees that the DOM is updated in a single, consistent batch, preventing users from ever seeing a partially updated or broken UI.

Because the Commit Phase runs only after a render has been finalized and is guaranteed to complete, it is the safe and correct place to run side effects. This includes tasks like making API calls, setting up subscriptions, or manually manipulating the DOM. The lifecycle methods that execute during this phase are specifically designed for these kinds of interactions.

Let’s explore these lifecycle methods using a CarBookingWidget component, which might need to interact with the DOM and fetch data after it renders.

class CarBookingWidget extends React.Component {
chatRef = React.createRef();

// 1. getSnapshotBeforeUpdate: Runs right before the DOM is updated.
// Its return value is passed to componentDidUpdate.
getSnapshotBeforeUpdate(prevProps, prevState) {
// Let's capture the scroll position of a chat log before a new message is added.
if (prevProps.messages.length < this.props.messages.length) {
const chatLog = this.chatRef.current;
return chatLog.scrollHeight - chatLog.scrollTop;
}
return null;
}

// 2. componentDidUpdate: Runs immediately after the update is committed to the DOM.
// Perfect for side effects that depend on the new props or the DOM being updated.
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot, we can use it to maintain the scroll position.
if (snapshot !== null) {
const chatLog = this.chatRef.current;
chatLog.scrollTop = chatLog.scrollHeight - snapshot;
}

// A common use case: Fetch new data when a prop like an ID changes.
if (this.props.carID !== prevProps.carID) {
fetch(`/api/cars/${this.props.carID}/addons`).then(/* ... */);
}
}

// 3. componentDidMount: Runs once, after the component is first mounted to the DOM.
// The ideal place for initial data loads and setting up subscriptions.
componentDidMount() {
// Example: Connect to a WebSocket for real-time price updates for this car.
this.subscription = setupPriceListener(this.props.carID, (newPrice) => {
this.setState({ price: newPrice });
});
}

// 4. componentWillUnmount: Runs right before the component is removed from the DOM.
// Essential for cleanup to prevent memory leaks.
componentWillUnmount() {
// Clean up the subscription when the widget is no longer needed.
this.subscription.unsubscribe();
}

render() {
// ... JSX for the booking widget ...
return <div ref={this.chatRef}>{/* ... messages ... */}</div>;
}
}

In this phase, you can be confident that the UI is in a consistent state. componentDidMount and componentDidUpdate are invoked after the DOM has been updated, so any DOM measurements you take will reflect the final layout. getSnapshotBeforeUpdate provides a unique window to capture information from the DOM before it changes. Finally, componentWillUnmount provides a critical hook to clean up any long-running processes when the component is destroyed. By strictly separating the pure calculations of the Render Phase from the side effects of the Commit Phase, React provides a powerful, predictable, and safe model for building complex applications.

Bringing It All Together

Our deep dive has taken us on a journey from the early days of React’s synchronous Stack Reconciler to the sophisticated, modern engine that powers today’s applications. We’ve seen how the limitations of an uninterruptible, recursive rendering process led to the creation of a groundbreaking new system. This system is built on the elegant interplay of three core components: the Fiber Reconciler, the Scheduler, and a distinct two-phase rendering process. Together, they form the foundation that makes React a powerful tool for building complex, high-performance user interfaces.

We’ve deconstructed the Fiber architecture, understanding that each “fiber” is not just a node in a tree, but a schedulable unit of work. Its pointer-based, linked-list structure is the key that unlocks the ability to pause, resume, or even abort rendering work without losing context. We then introduced the Scheduler, the intelligent traffic controller that prioritizes every update, ensuring that a user’s click is always handled before a background data fetch. Finally, we saw how this all comes together in the two-phase rendering model. The interruptible Render Phase safely calculates what needs to change without touching the DOM, while the synchronous Commit Phase applies those changes in one swift, consistent batch.

This advanced architecture is precisely why React can handle fluid animations, complex user interactions, and large-scale data updates without freezing the browser. It is the reason developers can build applications that feel fast and responsive, even when immense computational work is happening behind the scenes.

Understanding these internal mechanisms is more than just an academic exercise; it directly influences how we write better React code. Knowing that the Render Phase can be interrupted reinforces the critical importance of keeping our render methods and functional components pure and free of side effects. Recognizing that the Commit Phase is the safe place for mutations encourages the correct use of lifecycle methods and hooks like useEffect for API calls and subscriptions. When you use modern APIs like startTransition to wrap a non-urgent state update, you are directly tapping into the power of the Scheduler, telling it to treat that work as deferrable.

By grasping the “why” behind React’s architecture, we move beyond simply following patterns and begin to make informed decisions. We write more resilient, efficient, and performant code because we understand the elegant and powerful dance happening inside React every time our application’s state changes.

A Programmer’s Guide to Types and Data Structures in JavaScript


Data structures are fundamental tools in programming, enabling us to efficiently store, manipulate, and access data. In JavaScript, a language known for its flexibility, mastering these structures can significantly enhance your ability to solve problems and write optimal code.

In this blog post, we’ll explore commonly used data structures in JavaScript. By understanding both the “how” and the “why” of these data structures, you’ll be better equipped to tackle complex problems. As always, we start simple.


Primitive Types

JavaScript’s primitive types form the foundation of all data manipulation. We can consider that there are 3 main primitive data types: strings, numbers and booleans.

Strings

In JavaScript, strings are a fundamental data type used to represent textual data. A string is essentially a sequence of characters, numbered from 0, for the first character, enclosed within single quotes (’ ’), double quotes (” “), or backticks ( ). Strings are immutable, meaning once created, their content cannot be altered—any modification results in the creation of a new string.

You can create strings in several ways:

  • Using Single or Double Quotes
let singleQuoteString = 'Hello, World!';
let doubleQuoteString = "JavaScript is fun!";

Both are functionally identical. The choice is often based on stylistic preference or the need to include quotation marks within the string.

  • Using Backticks (Template Literals)

Backticks allow for multiline strings :

let multiline = `This is
a multiline
string.`;
console.log(multiline);

JavaScript uses something called escape sequences to include special characters within strings.

Escape SequenceDescription
\’Single quote
\”Double quote
\\Backslash
\nNewline
\tTab
\bBackspace
\rCarriage return
let quote = "She said, \"JavaScript is awesome!\"";
console.log(quote); // She said, "JavaScript is awesome!"

let multiline = "Line1\nLine2\nLine3";
console.log(multiline);
// Output:
// Line1
// Line2
// Line3

JavaScript uses UTF-16 encoding, allowing the use of Unicode characters.

let smiley = "\u263A";
console.log(smiley); // ☺

String manipulation will be addressed in a dedicated blog post.

Numbers

In JavaScript, numbers are a fundamental data type used to represent both integers and floating-point values. JavaScript has two primary numeric types:

  • Number (the default type for all numeric values)
  • BigInt (for very large integers beyond the safe range of Number)
let integer = 42;        // Integer
let float = 3.14; // Floating-point number
let negative = -100; // Negative number
let scientific = 1e6; // Scientific notation (1 * 10^6 = 1000000)
let bigNumber = 1234567890123456789012345678901234567890n;
console.log(bigNumber); // BigInt representation with 'n' at the end

You can represent numbers in different formats:

  • Decimal (Base 10):
let decimal = 255;
  • Binary (Base 2): Prefixed with 0b
let binary = 0b11111111; // 255 in binary
  • Octal (Base 8): Prefixed with 0o
let octal = 0o377; // 255 in octal
  • Hexadecimal (Base 16): Prefixed with 0x
let hex = 0xFF; // 255 in hexadecimal

There are some special numeric representations, like Infinity and -Infinity, which represent values beyond the maximum representable number.

console.log(1 / 0);  // Infinity
console.log(-1 / 0); // -Infinity
console.log(Infinity + 1); // Infinity
console.log(Infinity); // Infinity
console.log(Infinity + 1); // Infinity
console.log(Math.pow(10, 1000)); // Infinity
console.log(Math.log(0)); // -Infinity
console.log(1 / Infinity); // 0
console.log(1 / 0); // Infinity

Another special representation is NaN (Not-a-Number), which represents an invalid number operation.

console.log("abc" * 3); // NaN
console.log(0 / 0); // NaN
console.log(NaN === NaN); // false (NaN is not equal to itself)

To check for NaN, use:

console.log(isNaN("abc")); // true
console.log(Number.isNaN(123)); // false (preferred, as it's stricter)

Number operations are an extensive subject, covered in a dedicated blog post.

Booleans

In JavaScript, a boolean is a fundamental data type that represents one of two values: true or false. Booleans are essential for controlling program flow through conditional statements, loops, and logical operations. They form the backbone of decision-making processes in programming.

let isJavaScriptFun = true;
let isSkyGreen = false;

console.log(isJavaScriptFun); // true
console.log(isSkyGreen); // false

You can directly assign true or false or obtain boolean results from comparisons and logical operations.

JavaScript can convert other data types to booleans. Use the Boolean() function to explicitly convert values to booleans.

console.log(Boolean(1));        // true
console.log(Boolean(0)); // false
console.log(Boolean("hello")); // true
console.log(Boolean("")); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined));// false

In JavaScript, all values are either “truthy” or “falsy” when evaluated in a boolean context. The following values are considered “falsy” (evaluate to false in boolean contexts):

  • false
  • 0 and -0
  • “” (empty string)
  • null
  • undefined
  • NaN

Everything else is “truthy”, including:

  • Non-zero numbers (1, -1, etc.)
  • Non-empty strings (“hello”, “0”, etc.)
  • Objects ({}, [])
  • Functions

Example:

if ("") {
console.log("Truthy!");
} else {
console.log("Falsy!"); // Output: "Falsy!"
}

if ([]) {
console.log("Truthy!"); // Output: "Truthy!" (empty arrays are truthy)
}

NOT (!) Inverts the boolean value: true becomes false and vice versa.

console.log(!true);  // false
console.log(!false); // true

let isAvailable = false;
console.log(!isAvailable); // true

Booleans are a fundamental part of JavaScript, enabling conditional logic, control flow, and decision-making. Understanding truthy/falsy values, logical operators, and boolean conversions is crucial for writing clean, efficient code. Whether you’re toggling UI states, validating user input, or controlling program logic, booleans are at the heart of every JavaScript application.

Null and Undefined

In JavaScript, few things are as deceptively simple yet confusing as null and undefined. They both represent the absence of a value, but in subtly different ways. Think of them as distant cousins: related, yet with distinct personalities and behaviors. Understanding when and why to use each is crucial to avoid bugs that lurk in the shadows of type coercion and equality checks.

Undefined Indicates that a variable has been declared but has not been assigned a value. It’s the default value for uninitialised variables and missing function arguments.

let x;
console.log(x); // undefined

Null Represents an intentional absence of any object value. It’s an assignment value, meaning a developer explicitly sets it to indicate “no value.”

let y = null;
console.log(y); // null

Understanding the nuances of null and undefined is like learning the difference between a missing chair and an empty chair. Both suggest “no one’s sitting,” but for entirely different reasons.

Data Structures

Data structures can store collections of values and more complex entities. They are used to manipulate related data in several ways.

Arrays

At its core, an array is a special kind of object designed to hold multiple values in a single, ordered collection. Imagine a row of mailboxes, each assigned a number starting from zero (because JavaScript has a fondness for zero-based indexing). Each mailbox can contain letters, packages, or even a small raccoon if you’re into exotic pets—similarly, JavaScript arrays can hold numbers, strings, objects, functions, or even other arrays.

There are several ways to create an array in JavaScript. Using square brackets is the most common way:

let fruits = ["Apple", "Banana", "Cherry"];
let numbers = new Array(1, 2, 3, 4, 5);
let emptyArray = [];
let fixedArray = new Array(10); // An array with 10 empty slots

Since arrays are zero-indexed, the first element lives at index 0, the second at 1, and so on.

let cars = ["Toyota", "Honda", "Ford"];
console.log(cars[0]); // Outputs: "Toyota"

cars[1] = "Tesla"; // Changing "Honda" to "Tesla"
console.log(cars); // ["Toyota", "Tesla", "Ford"]

If you try to access an index that doesn’t exist, JavaScript politely returns undefined instead of throwing a tantrum.

console.log(cars[5]); // undefined

Arrays are dynamic, meaning you can add or remove elements on the fly, push() adds to the end, unshift() adds to the beginning, pop() removes from the end and shift() removes from the beginning:

cars.push("Chevrolet");
console.log(cars); // ["Toyota", "Tesla", "Ford", "Chevrolet"]
cars.unshift("BMW");
console.log(cars); // ["BMW", "Toyota", "Tesla", "Ford", "Chevrolet"]
let lastCar = cars.pop();
console.log(lastCar); // "Chevrolet"
console.log(cars); // ["BMW", "Toyota", "Tesla", "Ford"]
let firstCar = cars.shift();
console.log(firstCar); // "BMW"
console.log(cars); // ["Toyota", "Tesla", "Ford"]

Need to organize data hierarchically? Arrays can contain other arrays.

let matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

console.log(matrix[1][2]); // Outputs: 6

Arrays are such an extensive and complex topic that they will be addressed again in a dedicated blog post.

Objects

An object is a collection of key-value pairs. The keys (also called properties) are strings (or symbols), and the values can be anything: numbers, strings, functions, arrays, or even other objects.

There are a couple of ways to create objects in JavaScript. Using object literals is the most common way:

let car = {
brand: "Tesla",
model: "Model 3",
year: 2022
};

Here, brand, model, and year are the keys, and “Tesla”, “Model 3”, and 2022 are their respective values.

You can access object properties in two ways: dot notation and bracket notation. Bracket notation is particularly useful when dealing with dynamic property names.

console.log(car.brand); // "Tesla"

console.log(car["model"]); // "Model 3"
let property = "year";
console.log(car[property]); // 2022

You can also add new properties or modify existing ones:

car.color = "red";           // Adding a new property
car.year = 2020; // Updating an existing property
delete car.model; // Removing a property

Objects can contain other objects, allowing you to model more complex data structures:

let rentalCar = {
brand: "Tesla",
model: "Model Y",
owner: {
name: "Alice",
license: "XYZ1234"
}
};

console.log(rentalCar.owner.name); // "Alice"

Objects can do more than just store data—they can also perform actions. As with other complex topics, object swill be a addressed again in their very own, dedicated blog post.

Sets

A Set in JavaScript is like an exclusive party—each value is allowed in only once. No duplicates, no exceptions. It’s a collection of values where uniqueness is the rule, not the exception.

Creating a Set is quite simple:

let uniqueNumbers = new Set([1, 2, 3, 4, 4, 5]);
console.log(uniqueNumbers); // Set(5) {1, 2, 3, 4, 5}

Notice how the duplicate 4 politely disappeared? That’s the magic of Set—it automatically filters out duplicates. You can add values using the add() method:

uniqueNumbers.add(6);
console.log(uniqueNumbers); // Set(6) {1, 2, 3, 4, 5, 6}

To remove a value, use delete():

uniqueNumbers.delete(3);
console.log(uniqueNumbers); // Set(5) {1, 2, 4, 5, 6}

Want to clear the entire set? Just call clear():

uniqueNumbers.clear();
console.log(uniqueNumbers); // Set(0) {}

Sets will also be addressed in another blog post.

Maps

A Map is like a turbocharged object. It lets you use any data type as a key—whether it’s a string, number, object, or even a function. It also remembers the order in which you add items, which makes iteration predictable and intuitive.

You can create a Map as follows:

let carMap = new Map();
carMap.set("brand", "Tesla");
carMap.set("model", "Model 3");
carMap.set("year", 2022);

console.log(carMap);
// Map(3) {"brand" => "Tesla", "model" => "Model 3", "year" => 2022}

Alternatively, you can initialise a Map with an array of key-value pairs:

let userMap = new Map([
["name", "Alice"],
["age", 30],
["isMember", true]
]);

console.log(userMap.get("name")); // "Alice"

Add an entry with set(key, value):

userMap.set("city", "New York");

Retrieve a value with get(key):

console.log(userMap.get("age")); // 30

Remove an entry with delete(key):

userMap.delete("city");

Clear all entries with clear():

userMap.clear();

Unlike objects, Map allows keys of any type:

let objKey = { id: 1 };
let map = new Map();
map.set(objKey, "Object as a key");

console.log(map.get(objKey)); // "Object as a key"

While objects are great for simple, static key-value data, Maps shine when:

  • You need keys of any data type.
  • The insertion order of items matters.
  • You perform frequent additions and deletions of key-value pairs (Maps are faster for these operations compared to objects).

The Map object in JavaScript offers flexibility and performance that objects simply can’t match for dynamic key-value data management. We will be seeing a lot more about maps in a different blog post.

WeakMap and WeakSet

Both WeakSet and WeakMap are specialised and offer a unique feature: they allow for weak references to objects. This means that the garbage collector can automatically remove these objects from memory when they’re no longer needed elsewhere in your code. In simpler terms, they help prevent memory leaks without you having to do any heavy lifting.

A WeakSet is similar to a regular Set, but with a twist: It can only contain objects—no primitive values like strings or numbers. The objects are held weakly, meaning if there are no other references to an object, it can be garbage collected automatically.

let obj1 = { name: "Alice" };
let obj2 = { name: "Bob" };

let weakSet = new WeakSet([obj1, obj2]);

console.log(weakSet.has(obj1)); // true

// Removing external references
obj1 = null; // The object { name: "Alice" } is now eligible for garbage collection

Since WeakSet doesn’t prevent garbage collection, you won’t find methods like size, clear(), or any way to iterate over its elements. It’s designed for specific use cases like tracking objects without worrying about memory leaks.

A WeakMap is like a regular Map, but with some key differences: Keys must be objects (no primitives allowed). Keys are held weakly, meaning if there are no other references to a key, it can be garbage collected along with its associated value.

let user = { id: 1 };
let weakMap = new WeakMap();

weakMap.set(user, "User data");

console.log(weakMap.get(user)); // "User data"

user = null; // The key-value pair is eligible for garbage collection

Just like WeakSet, WeakMap doesn’t support iteration methods (forEach, keys, values, etc.) because the keys can disappear at any moment when garbage collected.

While Set and Map are versatile, they can accidentally cause memory leaks if you forget to manually remove references. WeakSet and WeakMap solve this by design—they automatically let go of objects when they’re no longer needed. They are very specialised and you will probably only use them in very specific situations.

Conclusion

Understanding and leveraging JavaScript’s data structures, along with their real-world applications, is essential for crafting efficient and effective solutions. By matching the right data structure to your problem, you’ll unlock new levels of productivity and maintainability in your code.

Dive into your next project and see how these data structures can simplify your tasks! Happy coding!