You've probably used React for a while.

You know how to create components, manage state, use Hooks, fetch data, and optimize performance.

But have you ever wondered what actually happens when you call setState?

How does React know which part of the interface needs to change?

Why do keys matter when rendering lists?

What is the Virtual DOM, and what does React Fiber do?

Understanding these concepts will help you write better React code, debug difficult problems, and understand why React behaves the way it does.

In this article, we'll explore the internal concepts behind React's rendering process.

1. What Is the Virtual DOM?

The DOM, or Document Object Model, is the browser's representation of an HTML document.

For example:

<div> <h1>Hello React</h1> <p>Welcome to my application</p> </div> 

The browser represents this structure as a tree of nodes.

When you manipulate the DOM directly, you can change elements, attributes, and text.

For example:

const heading = document.querySelector("h1"); heading.textContent = "Hello World"; 

React provides a declarative way to describe what the UI should look like.

function App() { return ( <div> <h1>Hello React</h1> <p>Welcome to my application</p> </div> ); } 

Instead of manually updating DOM nodes, you describe the desired UI, and React manages the updates.

So, What Is the Virtual DOM?

The term Virtual DOM is commonly used to describe React's in-memory representation of UI structure.

React elements are lightweight descriptions of what should appear in the interface.

For example:

const element = ( <h1 className="title"> Hello React </h1> ); 

Conceptually, this describes:

  • An element of type h1.
  • A className prop.
  • A text child.

React uses these descriptions as part of its rendering process.

An important distinction:

React elements are not actual DOM nodes.

They are JavaScript objects describing the UI.

Also, the Virtual DOM is not a browser feature. It is part of the way React models and manages UI updates.

2. Why Does React Use a Virtual Representation?

Imagine building a counter without React.

let count = 0; const button = document.querySelector("button"); button.addEventListener("click", () => { count++; document.querySelector("h1").textContent = count; }); 

You are responsible for knowing which DOM element needs updating.

Now imagine an application with:

  • Hundreds of components.
  • Shared state.
  • Conditional rendering.
  • Dynamic lists.
  • Nested components.
  • Complex interactions.

Manually managing every DOM update becomes difficult.

React lets you describe the UI based on state:

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

When the state changes, React calculates the next UI and determines which host changes are necessary.

This makes application code more predictable and easier to maintain.

However, don't assume that creating a Virtual DOM automatically makes every React application faster than every manually updated DOM application.

React's main benefit is its declarative programming model and its ability to coordinate updates efficiently.

3. Understanding the React Render Process

When a component's state changes, React doesn't immediately update every DOM element.

There is a process.

A simplified view looks like this:

  1. An update is scheduled.
  2. React renders the affected component tree.
  3. React reconciles the new result with the previous result.
  4. React commits necessary changes to the host environment.
  5. The browser performs its rendering work.

Let's examine these stages.

Step 1: An Update Is Scheduled

Consider:

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

When the user clicks the button, setCount schedules a state update.

React determines when to process that update according to its scheduling rules and priority.

Step 2: React Renders Components

React calls the relevant component functions to calculate the next UI.

function Counter() { const [count, setCount] = useState(0); console.log("Rendering Counter"); return <h1>{count}</h1>; } 

When the state changes, React calls the component again.

This is the render phase.

During rendering, React calculates what the UI should look like.

Step 3: React Reconciles the Result

React compares the new element tree with the previous one.

It determines which parts can be reused and which changes are necessary.

This process is called reconciliation.

Step 4: React Commits Changes

React applies the required changes to the host environment.

For React DOM, this means updating the browser DOM where necessary.

The render phase and commit phase are different.

Render phase: Calculate the next UI.

Commit phase: Apply the necessary changes.

A render does not necessarily result in a DOM mutation.

Step 5: The Browser Updates the Screen

After React commits DOM changes, the browser may need to perform layout, painting, and compositing work.

These browser operations are separate from React's component rendering.

This distinction is important when investigating performance problems.

4. What Is Reconciliation?

Reconciliation is the process React uses to determine how the previous rendered tree relates to the next rendered tree.

Imagine the current UI:

function App() { return ( <div> <h1>Hello</h1> <p>Welcome</p> </div> ); } 

Now the component changes:

function App() { return ( <div> <h1>Hello</h1> <p>Welcome to React</p> </div> ); } 

The structure is mostly the same.

Only the text inside the paragraph changed.

React can reuse the existing DOM structure and update the relevant text.

It doesn't need to remove the entire div and recreate everything.

This is one of the practical benefits of reconciliation.

React Uses Heuristics

Comparing two arbitrary trees perfectly can be expensive.

React uses heuristics to make reconciliation efficient.

Some important principles include:

  1. Elements of different types are generally treated as different structures.
  2. Elements of the same type can often be updated in place.
  3. Keys help React match items in collections.

Let's explore each one.

5. Why Element Types Matter

Consider this component:

function App() { const [showProfile, setShowProfile] = useState(true); return ( <div> {showProfile ? ( <UserProfile /> ) : ( <UserSettings /> )} </div> ); } 

When showProfile changes, React sees a different component type at that position.

It generally unmounts the previous component and mounts the new one.

This means:

  • The old component's local state is discarded.
  • Its cleanup logic runs where applicable.
  • The new component gets its own state.
  • Its mounting logic runs.

Now consider two elements of the same type:

function App({ title }) { return <h1>{title}</h1>; } 

If title changes, React can reuse the existing h1 DOM element and update its text.

The element type remains the same.

This is why component identity and element types matter.

6. Keys: One of the Most Important Concepts in React

If you've worked with React lists, you've probably seen this warning:

Each child in a list should have a unique "key" prop. 

But why does React need keys?

Consider:

function TodoList({ todos }) { return ( <ul> {todos.map((todo) => ( <li>{todo.text}</li> ))} </ul> ); } 

React sees a collection of similar elements.

Now imagine inserting a new item at the beginning:

Before:

Apple Banana Orange 

After:

Strawberry Apple Banana Orange 

Without stable keys, React has less information about which old item corresponds to which new item.

Keys provide that identity information.

Correct Usage

function TodoList({ todos }) { return ( <ul> {todos.map((todo) => ( <li key={todo.id}> {todo.text} </li> ))} </ul> ); } 

Now React can match items using their stable IDs.

Why Shouldn't We Use Array Indexes?

Consider:

{todos.map((todo, index) => ( <TodoItem key={index} todo={todo} /> ))} 

This might work for a list that never changes order.

But consider a list with local component state.

function TodoItem({ todo }) { const [completed, setCompleted] = useState(false); return ( <li> <input type="checkbox" checked={completed} onChange={(event) => setCompleted(event.target.checked)} /> {todo.text} </li> ); } 

Imagine checking the second item and then inserting a new item at the beginning.

If indexes are used as keys, React may associate existing component state with different logical items.

The checkbox state can appear on the wrong todo.

Using stable IDs avoids this problem.

When Is an Index Acceptable?

Using an index as a key can be acceptable when:

  • The list is static.
  • Items are never inserted or deleted.
  • Items are never reordered.
  • There is no identity-sensitive local component state.

Still, stable IDs are usually the better choice.

Keys Are Not Props

This is another important detail.

<ProductCard key={product.id} product={product} /> 

The key is a special React attribute.

It is not passed to the component as a normal prop.

If your component needs the ID, pass it separately:

<ProductCard key={product.id} id={product.id} product={product} /> 

7. Keys Can Also Reset Component State

Keys aren't only useful in lists.

They can also control component identity.

Consider:

function ProfilePage({ userId }) { return <UserProfile userId={userId} />; } 

If userId changes, React may preserve the component's local state because the component type and position remain the same.

Sometimes that's not what you want.

For example, perhaps a profile editor should start fresh when the user changes.

You can use a key:

function ProfilePage({ userId }) { return ( <UserProfile key={userId} userId={userId} /> ); } 

When the key changes, React treats it as a different component identity.

The previous component is removed, and a new one is mounted.

This resets its local state.

It's often cleaner than writing multiple Effects just to reset state when a different entity is selected.

8. What Is React Fiber?

Now let's talk about one of the most important architectural changes in React.

React Fiber is the reconciliation architecture introduced in React 16.

It changed how React represents and processes rendering work.

Before Fiber, React's rendering process was more difficult to interrupt and resume.

Fiber introduced a more flexible architecture that allows React to:

  • Break rendering work into smaller units.
  • Schedule work with different priorities.
  • Pause rendering work.
  • Resume work later.
  • Abandon outdated work.
  • Support concurrent rendering capabilities.

Fiber is an internal implementation detail, but understanding its purpose helps explain modern React behavior.

What Is a Fiber?

A Fiber is an internal JavaScript object representing work associated with a component or another part of the React tree.

A simplified Fiber structure can be thought of as containing information such as:

  • The component or element type.
  • Pending props.
  • Previous props.
  • State and update information.
  • Relationships with parent, child, and sibling Fibers.
  • Effect-related information.
  • Scheduling and priority information.

The real implementation is more complex.

You don't normally create or manipulate Fiber objects yourself.

How Are Fibers Connected?

Fiber nodes are connected using relationships such as:

  • return: the parent Fiber.
  • child: the first child Fiber.
  • sibling: the next sibling Fiber.

A simplified conceptual structure:

App | +-- Header | +-- Main | | | +-- Sidebar | | | +-- Content | +-- Footer 

This representation allows React to traverse the component tree and manage work.

9. The Current Tree and Work-In-Progress Tree

One useful concept in Fiber is that React can maintain two related versions of its tree.

They are commonly referred to as:

  • Current tree.
  • Work-in-progress tree.

The current tree represents the committed UI.

The work-in-progress tree represents the next version React is preparing.

A simplified process:

  1. React starts with the current tree.
  2. An update is scheduled.
  3. React prepares work on a work-in-progress tree.
  4. React processes the required rendering work.
  5. If the work completes successfully, React commits the result.
  6. The newly committed tree becomes the current tree.

This architecture helps React prepare updates without immediately changing the committed UI.

It is also part of how React supports concurrent rendering.

The exact internal mechanics are more complicated, but this mental model is useful.

10. What Is Concurrent Rendering?

Concurrent rendering is a capability that allows React to work on rendering updates in a more flexible way.

It does not mean React renders everything simultaneously on multiple CPU cores.

Instead, React can prioritize, pause, resume, or discard rendering work when appropriate.

Imagine a search page with:

  • An input field.
  • A very large result list.
  • Expensive filtering and rendering.

Typing should remain responsive even if updating the results requires more work.

React can use scheduling features to prioritize urgent updates.

For example:

import { useState, useTransition, } from "react"; function SearchPage({ products }) { const [input, setInput] = useState(""); const [search, setSearch] = useState(""); const [isPending, startTransition] = useTransition(); function handleChange(event) { const value = event.target.value; setInput(value); startTransition(() => { setSearch(value); }); } const filteredProducts = products.filter((product) => product.name.toLowerCase().includes(search.toLowerCase()) ); return ( <div> <input value={input} onChange={handleChange} placeholder="Search products..." /> {isPending && <p>Updating results...</p>} {filteredProducts.map((product) => ( <p key={product.id}> {product.name} </p> ))} </div> ); } 

The input update is urgent.

The search update is marked as a transition.

React can prioritize the urgent update while managing the transition work.

Important: Concurrent Rendering Is Not a Guarantee of Faster Code

Concurrent capabilities improve scheduling flexibility and responsiveness.

They don't automatically make expensive calculations cheaper.

If a calculation takes 500 milliseconds, React still needs an efficient way to perform that calculation.

You may need:

  • Better algorithms.
  • Memoization.
  • Pagination.
  • Virtualization.
  • Web Workers for CPU-intensive tasks.

Scheduling and computational efficiency are different concerns.

11. React Rendering Can Be Interrupted

This is one of the major ideas behind Fiber.

In suitable situations, React can pause rendering work and continue it later.

Imagine React is preparing a low-priority update.

Then the user performs an urgent interaction.

React can prioritize the urgent work rather than requiring every unit of the previous render to finish first.

It can also discard outdated work.

For example, imagine the user types:

R Re Rea Reac React 

If React is still preparing an update for Rea when a newer update arrives, it may abandon the outdated render and work toward the latest result.

This helps avoid wasting time on UI states that are no longer relevant.

There are two important qualifications:

  1. React rendering work can be interrupted in concurrent scenarios.
  2. The commit phase is synchronous; React does not partially commit a tree and then pause halfway through applying that commit.

Also, component render functions should remain pure.

A render can happen more than once, and work that never commits should not produce external side effects.

12. Why React Components Should Be Pure

React expects components to behave like pure functions with respect to their props, state, and context.

A pure component render should:

  • Return the same UI for the same inputs.
  • Avoid modifying external variables.
  • Avoid performing network requests.
  • Avoid directly changing the DOM.
  • Avoid updating unrelated state during rendering.

Bad example:

let total = 0; function Product({ price }) { total += price; return <p>${price}</p>; } 

Every render changes an external variable.

If React renders the component more than once, total may become incorrect.

Better:

function Product({ price }) { return <p>${price}</p>; } 

If you need to calculate a total, derive it from data:

function Cart({ products }) { const total = products.reduce( (sum, product) => sum + product.price, 0 ); return <h2>Total: ${total}</h2>; } 

This is predictable and safe.

What About Side Effects?

Side effects belong in appropriate places:

  • Event handlers for user-triggered actions.
  • Effects for synchronizing with external systems.
  • Framework-specific server or data-loading mechanisms where appropriate.

For example:

function SaveButton() { async function handleSave() { await fetch("/api/save", { method: "POST", }); } return ( <button onClick={handleSave}> Save </button> ); } 

The network request happens because of a user action, not because React happened to render the component.

This distinction is especially important in concurrent rendering.

13. Why Strict Mode May Render Components More Than Once

You may have noticed something strange during development.

A component logs twice:

function App() { console.log("App rendered"); return <h1>Hello</h1>; } 

Why?

If your application uses Strict Mode:

import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; createRoot(document.getElementById("root")).render( <StrictMode> <App /> </StrictMode> ); 

React performs additional development-only checks to help identify unsafe behavior.

In particular, Strict Mode can call component functions an extra time during development to help reveal impure rendering.

Effects also receive an extra setup-and-cleanup cycle in development under Strict Mode.

This is intentional.

It helps detect problems such as:

  • Missing cleanup logic.
  • Impure rendering.
  • Incorrect assumptions about component mounting.
  • Effects that don't safely handle repeated setup.

These development checks do not mean production components will behave identically.

Don't remove Strict Mode just because you see extra logs.

Instead, make your components and Effects safe.

14. What Happens When State Changes?

Let's look at a slightly more realistic example.

function Dashboard() { const [user, setUser] = useState({ name: "Alex", role: "Developer", }); function updateRole() { setUser({ ...user, role: "Senior Developer", }); } return ( <div> <h1>{user.name}</h1> <p>{user.role}</p> <button onClick={updateRole}> Update Role </button> </div> ); } 

When the user clicks the button:

  1. setUser schedules an update.
  2. React processes the update.
  3. Dashboard renders again.
  4. React creates the next element descriptions.
  5. React reconciles the new result.
  6. React commits the required DOM changes.

The browser doesn't need to replace the entire page.

The heading text is unchanged.

The paragraph text changes.

React can reuse the existing DOM elements and update the necessary text.

This is the combination of rendering, reconciliation, and committing.

15. Batching: Why Multiple State Updates Can Produce One Render

React also uses batching to group state updates.

Consider:

function Counter() { const [count, setCount] = ...
Source:https://dev.to/hamedfarazi/react-mastery-series-part-7-react-internals-virtual-dom-fiber-reconciliation-ki4