React Rendering

Understanding how React renders an application is one of the most important concepts for becoming confident with React.

When you change state, update props, or interact with a component, React needs to determine what the user interface should look like next. It then updates the browser so that the screen matches the latest React output.

At first, this process can seem complicated because React uses concepts such as rendering, reconciliation, the Virtual DOM, component identity, and the commit phase.

The good news is that these concepts are closely related.

A simplified view of React’s rendering process is:

State / Props / Context Change
            ↓
       React schedules
          an update
            ↓
        Render Phase
            ↓
   React calculates the UI
            ↓
       Reconciliation
            ↓
        Commit Phase
            ↓
    Browser DOM is updated
            ↓
       User sees the UI

Understanding this process also helps explain why React can preserve state in one situation but reset it in another, why changing a component’s key can reset state, and why unnecessary re-renders can affect performance.

How React Renders

React rendering is the process React uses to determine what the user interface should look like.

Consider this component:

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

When count changes, React needs to determine what should appear on the screen.

The important thing to understand is that React does not simply take the entire application and blindly rebuild the browser DOM every time.

Instead, React:

  1. Schedules an update.
  2. Calls components to determine their latest output.
  3. Compares the new result with the previous result.
  4. Determines what needs to change.
  5. Commits the necessary DOM updates.

This process allows React to keep the programming model declarative.

You describe what the UI should look like:

<button>
  Count: {count}
</button>

React handles the work required to keep the actual DOM synchronized with that description.

The Initial Render

When a React application starts, React performs an initial render.

For example:

import { createRoot } from "react-dom/client";
import App from "./App.jsx";

const root = createRoot(
  document.getElementById("root")
);

root.render(<App />);

The root.render() call tells React to render the application into the specified root.

React begins by rendering the component tree.

For example:

App
├── Header
├── Main
│   ├── ProductList
│   └── ProductCard
└── Footer

React evaluates the components and determines what the UI should look like.

The result is eventually committed to the browser DOM.

Render Phase

The render phase is when React calculates what the next version of the UI should look like.

During this phase, React calls components and evaluates their output.

For example:

function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}

When React renders:

<Greeting name="Alex" />

the component produces:

<h1>Hello, Alex</h1>

React uses this output as part of its work to determine the next UI.

Rendering Does Not Mean Updating the DOM

This distinction is extremely important.

The render phase is primarily about calculating the next UI.

It does not mean React immediately changes the browser DOM for every JSX expression it evaluates.

React separates the calculation from the actual DOM changes.

That separation gives React flexibility in how and when it performs work.

Components Should Be Pure During Rendering

React expects components to behave like pure functions during rendering.

For example:

function User({ name }) {
  return <h2>{name}</h2>;
}

Given the same inputs, the component should produce the same output.

Avoid performing side effects during rendering:

function User() {
  document.title = "User";
  
  return <h2>User</h2>;
}

Changing the document title is a side effect and should generally be handled through an appropriate Effect or event handler.

Similarly, avoid:

function Component() {
  fetch("/api/data");

  return <div>Loading...</div>;
}

Network requests should not normally be started directly during rendering.

Rendering should primarily calculate UI.

What Happens During the Render Phase?

Suppose you have:

function App() {
  return (
    <main>
      <Header />
      <Content />
    </main>
  );
}

React may render:

App
 ↓
main
 ├── Header
 └── Content

React evaluates the component tree to determine the next UI representation.

If a component returns another component:

function App() {
  return <Dashboard />;
}

React continues through the tree.

The render work can therefore involve many components.

However, React does not necessarily commit changes for every component that was rendered.

A component can render and ultimately result in no DOM change.

Commit Phase

After React finishes the necessary render work, it moves to the commit phase.

During the commit phase, React applies the necessary changes to the host environment.

For a browser application, this generally means updating the DOM.

For example, if the previous UI was:

Count: 0

and the new UI should be:

Count: 1

React commits the necessary DOM change.

The simplified process is:

Render
  ↓
Calculate next UI
  ↓
Compare with previous result
  ↓
Commit necessary changes
  ↓
DOM reflects the new UI

Effects and the Commit Phase

Effects are connected to the commit process.

For example:

useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]);

React first calculates and commits the UI.

The Effect then runs according to React’s Effect timing rules.

This is another reason why side effects should not be placed directly inside rendering logic.

Render Phase vs Commit Phase

A simple comparison:

PhaseMain Responsibility
Render PhaseCalculate what the UI should look like
Commit PhaseApply necessary changes to the DOM
EffectsSynchronize with external systems after commit according to their timing

Think of it like creating a plan and then applying that plan.

The render phase determines what should change.

The commit phase performs the actual UI changes.

Re-rendering

A re-render occurs when React needs to render a component again because something that affects its output has changed.

Common causes include:

  • state updates
  • changed props
  • context updates
  • a parent component rendering again

For example:

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

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

When the user clicks the button:

setCount(count + 1);

React schedules an update.

The component renders again with the new state.

Re-rendering Does Not Mean the Entire DOM Is Rebuilt

This is a common misunderstanding.

Suppose:

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

  return (
    <div>
      <h1>My Counter</h1>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

When count changes, React may render the component again.

But React does not need to recreate every DOM node from scratch.

It determines what changed and commits the necessary update.

The heading can remain unchanged while only the text displaying count needs to change.

Why Does a Parent Re-render Its Children?

Consider:

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

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>

      <Profile />
    </>
  );
}

When count changes, App renders again.

By default, React also evaluates the child component:

<Profile />

This does not necessarily mean React changes the DOM produced by Profile.

React may render Profile, compare its result, and determine that no DOM update is necessary.

This distinction is important:

A component rendering again is not the same thing as the browser DOM changing.

Reconciliation

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

Imagine the previous output:

<ul>
  <li>Apple</li>
  <li>Banana</li>
</ul>

and the next output:

<ul>
  <li>Apple</li>
  <li>Orange</li>
</ul>

React can determine that the list structure remains largely the same and that the second item’s content has changed.

It can then update the necessary part of the DOM.

Reconciliation Is Based on Identity

React needs a way to determine whether something in the new tree represents the same thing as something in the previous tree.

Elements of the same type in the same position will often be treated as the same underlying component or DOM element, subject to keys and tree structure.

For example:

<Counter />

followed by another render that still has:

<Counter />

at the same position can preserve the component’s state.

But changing its identity can cause React to treat it as a different component.

Keys and Reconciliation

Keys are particularly important when rendering lists.

Consider:

{users.map(user => (
  <UserCard
    key={user.id}
    user={user}
  />
))}

The key helps React identify each item across renders.

Suppose the data changes from:

A
B
C

to:

C
A
B

With stable keys:

A → key="a"
B → key="b"
C → key="c"

React can understand that the same items have moved.

Without appropriate keys, React has less information about item identity.

Why Index Keys Can Be Problematic

Consider:

{items.map((item, index) => (
  <Item key={index} item={item} />
))}

If items can be inserted, deleted, or reordered, an index can refer to a different item after a change.

This can lead to incorrect state being associated with the wrong item.

Prefer a stable identifier from the data:

{items.map(item => (
  <Item
    key={item.id}
    item={item}
  />
))}

Index keys can be acceptable for truly static lists where items never reorder, are never inserted or removed, and have no identity beyond their position.

Component Identity

Component identity determines whether React should treat a component in a new render as the same component as before or as a new component.

This is directly connected to state preservation.

Consider:

function App() {
  return <Counter />;
}

On the next render:

function App() {
  return <Counter />;
}

React can treat the Counter at that position as the same component.

Therefore, its state can be preserved.

But if the component type changes:

function App({ showCounter }) {
  return showCounter ? <Counter /> : <Profile />;
}

then switching between:

<Counter />

and:

<Profile />

changes the component type.

React treats them as different components.

Their state is not shared.

Component Position Matters

Consider:

function App({ isLoggedIn }) {
  return (
    <main>
      {isLoggedIn ? <Profile /> : <Login />}
    </main>
  );
}

The component at that position changes between Profile and Login.

React treats the different component types as different identities.

Now consider:

function App({ isLoggedIn }) {
  return (
    <main>
      <Profile />
    </main>
  );
}

Even if some props change, the Profile component remains at the same position and keeps its identity.

This is why React can preserve its state.

Virtual DOM

The term Virtual DOM refers to React’s in-memory representation of the UI.

When React renders JSX, it creates React elements that describe what the UI should look like.

For example:

<button className="primary">
  Save
</button>

describes a button element and its properties.

React uses these descriptions as part of its rendering and reconciliation process.

The browser, however, works with the actual DOM.

The conceptual relationship is:

React Components
      ↓
React Elements
      ↓
React's internal tree
      ↓
Reconciliation
      ↓
Browser DOM

Is the Virtual DOM Just a Copy of the DOM?

Not exactly.

It is better to think of React’s rendered representation as a lightweight description of the UI rather than a literal duplicate of every browser DOM detail.

React maintains its own internal structures to manage rendering and reconciliation.

The phrase “Virtual DOM” is useful for understanding the general idea, but modern React’s internals are more sophisticated than simply comparing two JavaScript DOM copies.

Why Does React Use This Approach?

It allows developers to write declarative UI:

return <h1>{name}</h1>;

instead of manually manipulating the DOM:

document.querySelector("h1").textContent = name;

React can determine how to transition the UI from one state to another.

This abstraction makes complex interfaces easier to reason about.

Virtual DOM Does Not Automatically Mean Faster

It is common to hear:

“React is fast because of the Virtual DOM.”

That is an oversimplification.

React’s performance comes from several aspects of its architecture, including its rendering model, reconciliation, scheduling, component structure, and the ability to avoid unnecessary work.

Using React does not automatically make every application fast.

Poor component architecture can still cause unnecessary rendering work.

For example:

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

  return (
    <>
      <Counter count={count} />
      <LargeComponent />
    </>
  );
}

If LargeComponent performs expensive rendering work whenever its parent renders, the application may need optimization depending on the actual performance profile.

State Preservation

One of React’s most useful behaviors is preserving state when a component retains its identity.

Consider:

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

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

If the component remains the same component at the same position in the tree, React can preserve:

count

across renders.

For example:

function App({ theme }) {
  return (
    <div className={theme}>
      <Counter />
    </div>
  );
}

Changing theme does not replace the Counter component.

The Counter keeps its state.

State Belongs to a Position in the Render Tree

A useful way to think about React state is:

State is associated with a component’s position and identity in the rendered tree.

Consider:

function App() {
  return (
    <section>
      <Counter />
    </section>
  );
}

The Counter occupies a particular position under section.

As long as React continues to see the same component identity at that position, its state can be preserved.

This explains many behaviors that otherwise seem surprising.

State Preservation When Props Change

Consider:

function Counter({ title }) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>{title}</h2>

      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>
    </div>
  );
}

Now:

<Counter title="First" />

changes to:

<Counter title="Second" />

The prop changed, but the component type and position did not.

Therefore, React can preserve the state.

The count does not automatically reset just because title changed.

State Resetting

Sometimes you intentionally want a component’s state to reset.

There are several ways this can happen.

One common method is changing the component’s key.

Consider:

function App({ userId }) {
  return <ProfileForm key={userId} userId={userId} />;
}

If:

userId = 1

the component has:

key="1"

If it changes to:

userId = 2

the key changes.

React treats the component as a different identity, so its state can reset.

This can be useful for forms.

Resetting a Form with a Key

Suppose:

function ProfileForm({ user }) {
  const [name, setName] = useState(user.name);

  return (
    <input
      value={name}
      onChange={event => setName(event.target.value)}
    />
  );
}

If the user changes from Alice to Bob, you might want the form to initialize again using Bob’s data.

You can use:

<ProfileForm
  key={user.id}
  user={user}
/>

When user.id changes, React gives the form a new identity.

Its state starts again from the new user’s initial value.

Keys Are Not Only for Lists

Many developers learn key through lists:

items.map(item => (
  <Item key={item.id} />
))

But keys can also intentionally control component identity.

For example:

<Chat key={recipient.id} recipient={recipient} />

Changing the recipient’s key can reset the chat component’s state.

This can be a useful technique when switching between independent entities.

Resetting State by Changing Component Type

State can also reset when the component type changes.

For example:

function App({ showCounter }) {
  return showCounter ? (
    <Counter />
  ) : (
    <Profile />
  );
}

Switching between Counter and Profile changes the component type.

React does not treat them as the same component.

Therefore, their states are independent.

Resetting State by Changing Position

Tree structure also affects identity.

Consider:

function App({ showExtra }) {
  return (
    <div>
      {showExtra && <Banner />}
      <Counter />
    </div>
  );
}

When Banner appears or disappears, the Counter remains the second child or first child depending on the rendered structure.

React’s reconciliation behavior considers the resulting tree structure and element identity.

A safer mental model is to think about the actual rendered tree and the identity of elements at each position rather than assuming state always belongs to a component name.

Explicitly Giving Components Different Identities

You can intentionally give two instances different identities with keys.

For example:

function App() {
  const [selectedUser, setSelectedUser] = useState("alice");

  return (
    <>
      <button onClick={() => setSelectedUser("alice")}>
        Alice
      </button>

      <button onClick={() => setSelectedUser("bob")}>
        Bob
      </button>

      <Chat
        key={selectedUser}
        recipient={selectedUser}
      />
    </>
  );
}

When selectedUser changes, the key changes.

React therefore creates a new Chat identity and resets its state.

State Preservation vs State Resetting

SituationTypical Result
Same component type at same positionState can be preserved
Props changeState is usually preserved
Parent re-rendersChild state can be preserved
Component type changesState resets for the replaced component
Key changesComponent identity changes and state resets
Different keyed instancesEach identity has separate state
List item moves with stable keyState can follow the keyed item

The key idea is identity, not simply whether the JSX looks similar.

Rendering and Performance

Rendering is a normal part of React applications.

A re-render is not automatically a performance problem.

The important question is:

How much work does React need to perform, and how often?

Consider:

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

  return (
    <>
      <Counter count={count} />
      <ProductList />
    </>
  );
}

If App re-renders, React may also evaluate ProductList.

If ProductList is large or performs expensive calculations, repeated rendering could become noticeable.

However, you should not automatically optimize every component.

Measure first.

Avoiding Unnecessary Rendering Work

One important strategy is good component design.

Instead of keeping unrelated state high in the tree:

function App() {
  const [search, setSearch] = useState("");

  return (
    <LargeApplication
      search={search}
      setSearch={setSearch}
    />
  );
}

you may sometimes place state closer to the component that actually needs it.

For example:

function SearchPanel() {
  const [search, setSearch] = useState("");

  return (
    <input
      value={search}
      onChange={event => setSearch(event.target.value)}
    />
  );
}

Keeping state local can reduce the amount of the tree affected by updates.

This is often called state colocation.

Component Memoization

React provides memo for components that can benefit from skipping renders when their props have not changed.

For example:

import { memo } from "react";

const ProductList = memo(function ProductList({
  products
}) {
  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
        </li>
      ))}
    </ul>
  );
});

If the parent renders again but the products prop remains referentially equal, React can skip rendering the memoized component in appropriate cases.

However, memo is an optimization, not a requirement.

If the component is inexpensive to render, memoizing it may provide little benefit.

useMemo and Rendering Performance

useMemo can cache the result of an expensive calculation.

For example:

const filteredProducts = useMemo(() => {
  return products.filter(product =>
    product.name
      .toLowerCase()
      .includes(search.toLowerCase())
  );
}, [products, search]);

The calculation can be skipped when its dependencies have not changed.

But useMemo should not be used simply because a component renders.

It is most useful when:

  • the calculation is meaningfully expensive
  • the dependencies often remain unchanged
  • measurement shows the calculation is worth optimizing

useCallback and Rendering Performance

useCallback caches a function reference.

For example:

const handleSelect = useCallback((id) => {
  setSelectedId(id);
}, []);

This can be useful when passing callbacks to memoized children or when a stable function reference is otherwise important.

But wrapping every function in useCallback can make code more complicated without producing meaningful performance improvements.

Rendering Performance and Large Lists

Large lists can create significant rendering work.

Consider:

{products.map(product => (
  <ProductCard
    key={product.id}
    product={product}
  />
))}

If there are thousands of items, rendering all of them at once may be expensive.

Possible techniques include:

  • pagination
  • filtering
  • virtualization
  • list windowing
  • component memoization where appropriate
  • moving expensive calculations out of rendering

The correct solution depends on the application’s actual performance problem.

Avoiding Expensive Work During Rendering

Consider:

function Dashboard({ data }) {
  const processed = expensiveCalculation(data);

  return <Chart data={processed} />;
}

If expensiveCalculation() takes significant time, every render may repeat the work.

If the calculation depends only on specific values, useMemo may help:

function Dashboard({ data }) {
  const processed = useMemo(() => {
    return expensiveCalculation(data);
  }, [data]);

  return <Chart data={processed} />;
}

But again, optimization should be based on an actual need.

State Updates Are Not Immediate Variable Changes

Consider:

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

  function handleClick() {
    setCount(count + 1);

    console.log(count);
  }

  return (
    <button onClick={handleClick}>
      {count}
    </button>
  );
}

The console may still show:

0

during that event handler.

Why?

Because the count variable belongs to the current render.

Calling:

setCount(count + 1);

requests an update.

React will render the component again with the new state.

This is part of React’s snapshot-based rendering model.

Multiple State Updates

Consider:

function handleClick() {
  setCount(count + 1);
  setCount(count + 1);
}

Both updates use the count value from the current render.

If you need each update to build on the previous update, use a functional update:

function handleClick() {
  setCount(value => value + 1);
  setCount(value => value + 1);
}

Now React can apply the updates based on the previous state values.

Rendering Is Not the Same as Painting

There are several layers involved in getting pixels onto the screen.

A simplified browser workflow is:

React Render
     ↓
React Commit
     ↓
DOM Changes
     ↓
Browser Rendering Work
     ↓
Pixels on Screen

React is responsible for its rendering and commit work.

The browser then handles its own layout, style calculation, painting, and compositing processes.

Therefore, an application can have a React performance problem or a browser rendering problem, and the two are not always the same.

Common Rendering Misconceptions

“Every Re-render Rebuilds the Entire DOM”

Not true.

React can render components again without replacing the entire DOM.

It calculates the necessary changes and commits the required updates.

“The Virtual DOM Is the Actual DOM”

Not true.

React maintains an internal representation of the UI and uses it as part of its rendering process.

The browser still has its own actual DOM.

“Every Re-render Is Bad”

Not true.

Rendering is a normal part of React.

The goal is not to eliminate all renders.

The goal is to avoid unnecessary expensive work.

“Changing Props Always Resets State”

Not true.

If the component retains its identity, changing props generally does not reset its state.

“Changing a Key Only Helps Lists”

Not true.

Keys identify elements and can also intentionally control component identity and state resetting.

“React Only Renders When State Changes”

Not exactly.

A component can render because:

  • its own state changes
  • its parent renders
  • its props change
  • consumed context changes
  • another update causes React to render that part of the tree

The exact behavior can also depend on optimizations such as memoization.

A Practical Rendering Example

Consider this application:

import { useState } from "react";

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

  return (
    <button onClick={() => setCount(value => value + 1)}>
      Count: {count}
    </button>
  );
}

function Profile({ name }) {
  return <h2>{name}</h2>;
}

function App() {
  const [name, setName] = useState("Alex");

  return (
    <main>
      <Profile name={name} />

      <button onClick={() => setName("Sam")}>
        Change Name
      </button>

      <Counter />
    </main>
  );
}

When the name changes:

App renders
   ↓
Profile receives new prop
   ↓
Profile renders with new name
   ↓
Counter remains the same component identity
   ↓
Counter state can be preserved

When the counter changes:

Counter state update
       ↓
Counter renders again
       ↓
New count is calculated
       ↓
React commits necessary DOM changes

The entire application does not need to be replaced in the browser.

A Mental Model for React Rendering

When debugging a rendering issue, think through these questions:

What caused the update?

Was it:

State?
Props?
Context?
Parent render?
External store?

Which components render?

Determine which components React needs to evaluate.

What changed?

Ask whether the resulting UI is actually different.

What gets committed?

React may render something without needing to change the DOM.

Is the work expensive?

A render only becomes a performance concern when the work is expensive enough or frequent enough to matter.

Can the component structure be improved?

Before adding memoization, consider:

  • moving state closer to where it is needed
  • splitting large components
  • avoiding unnecessary Effects
  • avoiding unnecessary state
  • keeping data transformations efficient

Rendering and Performance Best Practices

Keep Components Pure

Rendering should primarily calculate UI.

Avoid side effects during rendering.

Keep State Local When Possible

Do not automatically move every piece of state to the top of the application.

Keep state near the components that use it when practical.

Use Stable Keys

For dynamic lists, use stable identifiers:

key={item.id}

rather than relying on array indexes when items can change order.

Avoid Unnecessary State

If a value can be calculated from existing props or state, you often do not need separate state for it.

Avoid Unnecessary Effects

Effects are for synchronizing with external systems.

Do not use an Effect simply to calculate derived values.

Measure Before Optimizing

Use profiling and performance measurements to identify expensive work.

Do not assume that every re-render is a problem.

Use Memoization Intentionally

Use:

memo
useMemo
useCallback

when they address a meaningful performance issue.

Optimize Large Data Sets

For large lists or expensive data processing, consider:

Pagination
Virtualization
Memoization
Efficient data structures
Server-side filtering
Caching

depending on the application’s requirements.

Rendering Cheat Sheet

Initial Render
     ↓
React renders component tree
     ↓
Render Phase
     ↓
Calculate next UI
     ↓
Reconciliation
     ↓
Commit Phase
     ↓
DOM updates
     ↓
Browser displays result

When state changes:

setState()
   ↓
Update scheduled
   ↓
Component renders again
   ↓
React determines differences
   ↓
Necessary changes committed

Component identity:

Same type
+ same position
+ compatible key
        ↓
State can be preserved

Identity change:

Different type
or different key
        ↓
Different component identity
        ↓
State can reset

Key Takeaways

React rendering is the process through which React determines what the UI should look like and updates the browser when necessary.

The render phase calculates the next UI. Components are evaluated during this phase, and rendering should remain free of side effects.

The commit phase applies the necessary changes to the browser DOM.

A re-render means React needs to evaluate a component again. It does not mean React throws away and rebuilds the entire DOM.

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

The Virtual DOM is a useful conceptual description of React’s in-memory UI representation. React uses its internal representation and reconciliation process to manage updates efficiently.

Component identity is especially important for understanding state.

When React sees the same component identity in the appropriate position, state can be preserved.

When identity changes, such as through a different component type or a changed key, React can reset that component’s state.

Keys are especially important for dynamic lists because they allow React to identify individual items across renders. Stable data identifiers are generally preferred over indexes when list items can change order or be inserted or removed.

Finally, rendering itself is not automatically a performance problem.

Good React performance comes from reducing unnecessary expensive work, keeping state appropriately scoped, avoiding unnecessary Effects, using stable keys, designing components carefully, and applying memoization only when it provides a meaningful benefit.

Once you understand rendering, reconciliation, identity, and state preservation, many React behaviors that initially seem mysterious become much easier to predict.

Leave a Comment