DEV Community

Vijesh KR
Vijesh KR

Posted on

React Native Rendering: Understanding What Happens Between React Code and the Screen

When I first started using React Native, I understood the code I was writing.

I knew what this meant:

<View>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

I knew that View was something like a container and Text displayed text.

But I had never really asked a deeper question:

What actually happens between my React code and the pixels I see on the screen?

The code looks simple.

The result looks simple.

But there is a lot happening in between.

React Native has to take the React components we write in JavaScript, understand their structure, calculate where everything should appear, figure out what actually changed, and finally update the real native views on Android or iOS.

This is where concepts like Reconciliation, Fabric, Shadow Tree, Yoga, Commit, and Mount start to make sense.

Instead of learning these as separate terms, I found it much easier to understand them as different parts of the same journey.

So let's follow that journey.

High-level overview showing React Code → Renderer/Fabric → Shadow Tree → Layout → Commit → Mount → Native UI → Screen. This is the main architecture image.


The Code We Write

Let's start with something very simple.

function Welcome() {
  return (
    <View>
      <Text>Hello</Text>
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

At the React level, this is just a component.

React sees something like:

Welcome
   ↓
<View>
   ↓
<Text>Hello</Text>
Enter fullscreen mode Exit fullscreen mode

But Android and iOS don't understand React components.

Android doesn't have a native component called <View> in the React sense.

iOS doesn't know what <Text> means.

The operating system understands native UI objects.

So there is an important gap:

React Code
    ↓
    ?
    ↓
Native Views
    ↓
  Pixels
Enter fullscreen mode Exit fullscreen mode

What happens in that ?

That is the rendering system.

Visual explanation of the gap between React components like <View> / <Text> and actual Android/iOS native views.


Why Does React Native Need a Rendering System?

If React already knows what the UI should look like, why can't React just directly create the native views?

Because there is more to rendering a UI than creating views.

Consider this:

<View style={{ flex: 1 }}>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

Before Android or iOS can display this correctly, React Native needs to know things like:

  • What is the parent-child structure?
  • What size should the View have?
  • Where should the Text be positioned?
  • How much space is available?
  • What happens if the screen size changes?
  • What changed if state updates?
  • Which native views actually need to be created?
  • Which existing views can simply be updated?

React Native therefore needs an intermediate representation and a rendering process.

This is one of the most important ideas to understand:

React describes what the UI should look like. React Native's renderer figures out how that description becomes native UI.

And in the New Architecture, that renderer is called Fabric.

But before getting to Fabric, there is another React concept we need to understand.


Reconciliation: What Actually Changed?

Imagine we have a counter.

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

  return (
    <View>
      <Text>{count}</Text>

      <Button
        title="Increase"
        onPress={() => setCount(count + 1)}
      />
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Initially:

0
Enter fullscreen mode Exit fullscreen mode

After pressing the button:

1
Enter fullscreen mode Exit fullscreen mode

React doesn't need to throw away the entire UI and build everything again.

It needs to understand what changed.

Conceptually:

Previous UI

<View>
  <Text>0</Text>
  <Button />
</View>


New UI

<View>
  <Text>1</Text>
  <Button />
</View>
Enter fullscreen mode Exit fullscreen mode

The structure is mostly the same.

Only the text changed.

This process of React determining what the updated UI should look like is part of what we commonly call reconciliation.

The important idea is:

A React update does not mean every native view must be recreated.

The renderer can compare the previous and next representations and eventually apply only the necessary native changes.

This becomes especially important when an application becomes large.

Imagine a screen with:

Header
Profile
Statistics
Charts
Transactions
Buttons
Footer
Enter fullscreen mode Exit fullscreen mode

If one transaction amount changes, we don't want to rebuild everything.

We want the renderer to understand the difference.

This is where Fabric becomes important.


Introducing Fabric

Fabric is React Native's new rendering system.

It is not a completely separate UI framework.

It is the rendering system that connects React's world with the native platform.

The architecture was redesigned around a shared C++ core, better interoperability with native platforms, and capabilities needed for modern React Native.

A simplified mental model looks like this:

React Components
       ↓
React Reconciliation
       ↓
   Fabric Renderer
       ↓
    Shadow Tree
       ↓
  Layout Calculation
       ↓
      Commit
       ↓
      Mount
       ↓
      Screen
Enter fullscreen mode Exit fullscreen mode

But there is something important hiding inside this diagram.

Fabric doesn't immediately create native views when React renders.

Instead, it builds an intermediate representation.

This is the React Shadow Tree.


The Shadow Tree

When I first heard the term "Shadow Tree", I thought it was something complicated.

The basic idea is actually quite simple.

Suppose we write:

<View>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

Conceptually, React has a tree:

View
 └── Text
Enter fullscreen mode Exit fullscreen mode

Fabric creates a corresponding tree of Shadow Nodes.

ViewShadowNode
      │
      └── TextShadowNode
Enter fullscreen mode Exit fullscreen mode

A React Shadow Tree is created by Fabric and consists of React Shadow Nodes.

These nodes represent the React UI and contain information needed by the renderer, including props and layout information.

So instead of immediately saying:

"Create an Android View."

Fabric first builds a structured representation of what needs to exist.

This gives the renderer a place to reason about the UI before touching the actual native views.

Think of it like a plan.

React Code

<View>
  <Text>Hello</Text>
</View>

       ↓

Shadow Tree

ViewShadowNode
      │
      └── TextShadowNode

       ↓

Native Views

Android / iOS Views
Enter fullscreen mode Exit fullscreen mode

This separation is extremely useful.

Because now React Native can calculate, compare, and prepare changes before applying them to the actual UI.

Shows a React component hierarchy becoming ViewShadowNode → TextShadowNode, explaining the intermediate representation used by Fabric.


But Where Does Layout Come From?

We now know that the Shadow Tree represents the UI.

But it still doesn't answer an important question.

Where exactly should each component appear?

For example:

<View
  style={{
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  }}
>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

How does React Native decide:

View:
x = 0
y = 0
width = 390
height = 844

Text:
x = ...
y = ...
width = ...
height = ...
Enter fullscreen mode Exit fullscreen mode

This is where Yoga comes in.


Yoga and Layout Calculation

React Native uses Yoga as its layout engine for calculating layout information.

You can think of Yoga as the part responsible for answering:

"Given these styles and these available constraints, where should everything go?"

For example:

<View
  style={{
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  }}
>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

Yoga helps calculate the position and size of the elements.

Conceptually:

Available screen
      ↓
Parent constraints
      ↓
    Styles
      ↓
     Yoga
      ↓
x / y / width / height
Enter fullscreen mode Exit fullscreen mode

The result becomes layout information associated with the Shadow Tree.

It might conceptually look like:

View
x: 0
y: 0
width: 390
height: 844

   └── Text
       x: 160
       y: 400
       width: 70
       height: 25
Enter fullscreen mode Exit fullscreen mode

The exact values obviously depend on the device and content.

The important part is the responsibility.

Yoga calculates layout. Fabric uses that layout information as part of turning the Shadow Tree into native UI.

Shows how styles, constraints, and the component tree go through Yoga to produce x, y, width, and height.


Render, Commit, Mount

At this point, we have enough context to understand the three words that appear again and again in React Native's rendering architecture:

Render.

Commit.

Mount.

These are not three unrelated concepts.

They are three phases of the rendering pipeline.

React Native's rendering pipeline can be understood as:

Render → Commit → Mount
Enter fullscreen mode Exit fullscreen mode

Let's understand each one.


1. Render

Suppose we write:

<View>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

React executes our application logic and produces the React Element Tree in JavaScript.

Conceptually:

React Component
      ↓
React Element Tree
Enter fullscreen mode Exit fullscreen mode

Fabric then creates the corresponding React Shadow Tree.

React Element Tree
      ↓
React Shadow Tree
Enter fullscreen mode Exit fullscreen mode

So the Render phase is essentially about constructing the next representation of the UI.

A simplified mental model is:

JavaScript / React
        ↓
  React Elements
        ↓
   Shadow Nodes
        ↓
    Shadow Tree
Enter fullscreen mode Exit fullscreen mode

The Shadow Tree is immutable.

That means React Native does not simply mutate the existing tree whenever something changes.

Instead, it creates a new version of the tree, while using structural sharing so unchanged portions don't need to be duplicated unnecessarily.

That sounds complicated, but the reason is important.

It makes the rendering system easier to reason about and allows different versions of UI state to be handled safely.


2. Commit

Once the new Shadow Tree has been created, React Native needs to prepare it for mounting.

This is the Commit phase.

Two important things happen here:

  1. Layout calculation
  2. Preparing the next tree for mounting

Yoga calculates the layout of the Shadow Tree using the available constraints and styles.

Then the newly prepared tree becomes the next tree that can be mounted.

Conceptually:

Shadow Tree
     ↓
    Yoga
     ↓
Layout information
     ↓
  Next Tree
Enter fullscreen mode Exit fullscreen mode

So after Commit, React Native has something much closer to:

"This is the UI tree we want to display, and this is where everything should be."

But we still haven't actually updated the native views.

That happens next.


3. Mount

Now we finally reach the native UI.

The Mount phase takes the Shadow Tree, including its calculated layout, and turns it into the native Host View Tree.

This is where actual native views are created, updated, removed, or rearranged.

For example:

Shadow Node
     ↓
Native View
Enter fullscreen mode Exit fullscreen mode

For Android, this can result in native Android views.

For iOS, corresponding UIKit views are used.

The renderer also compares the previously rendered tree with the next tree and produces the required mutations.

Conceptually:

Previous Tree
      +
  Next Tree
      ↓
   Tree Diff
      ↓
create / update / remove
      ↓
  Native Views
Enter fullscreen mode Exit fullscreen mode

Finally:

Native Views
      ↓
    Screen
Enter fullscreen mode Exit fullscreen mode

Focused diagram explaining the three major phases: Render → Commit → Mount, with what each phase does.


Following One Example From Start to Finish

Let's put everything together.

Suppose we have:

function Greeting() {
  return (
    <View>
      <Text>Hello</Text>
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

What happens?

Step 1 — React

React executes the component.

Greeting
   ↓
<View>
   ↓
<Text>Hello</Text>
Enter fullscreen mode Exit fullscreen mode

React creates the React Element Tree.


Step 2 — Fabric

Fabric creates the corresponding Shadow Tree.

ViewShadowNode
      │
      └── TextShadowNode
Enter fullscreen mode Exit fullscreen mode

Step 3 — Layout

Yoga calculates the layout.

View
 ├── x
 ├── y
 ├── width
 └── height

Text
 ├── x
 ├── y
 ├── width
 └── height
Enter fullscreen mode Exit fullscreen mode

Step 4 — Commit

The newly prepared tree becomes the next tree to be mounted.


Step 5 — Mount

The renderer determines the required native operations.

Conceptually:

Create View
Create Text
Set Text = "Hello"
Add Text to View
Enter fullscreen mode Exit fullscreen mode

Step 6 — Screen

The native platform displays the result.

Hello
Enter fullscreen mode Exit fullscreen mode

That's the entire journey.

React Component
      ↓
React Element Tree
      ↓
Reconciliation
      ↓
Shadow Tree
      ↓
Yoga Layout
      ↓
Commit
      ↓
Mount
      ↓
Native Views
      ↓
    Screen
Enter fullscreen mode Exit fullscreen mode

Follows one <View><Text>Hello</Text></View> example from React component → Element Tree → Shadow Tree → Yoga → Commit → Mount → Native View → Screen.


What Happens When State Changes?

The initial render is only half the story.

The more interesting case is an update.

Consider:

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

  return (
    <View>
      <Text>{count}</Text>

      <Button
        title="Increase"
        onPress={() => setCount(count + 1)}
      />
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Initially:

0
Enter fullscreen mode Exit fullscreen mode

The user taps the button.

Now:

1
Enter fullscreen mode Exit fullscreen mode

What happens?


First: React Updates

The state changes.

count: 0
      ↓
count: 1
Enter fullscreen mode Exit fullscreen mode

React produces a new version of the React Element Tree.

Conceptually:

Previous

<View>
  <Text>0</Text>
  <Button />
</View>


Next

<View>
  <Text>1</Text>
  <Button />
</View>
Enter fullscreen mode Exit fullscreen mode

The important thing is that React doesn't need to recreate everything conceptually from scratch.

The unchanged parts can be shared between the old and new trees.


Then: Shadow Tree Update

Fabric creates the updated Shadow Tree.

Conceptually:

Old Shadow Tree

View
 ├── Text("0")
 └── Button


New Shadow Tree

View
 ├── Text("1")
 └── Button
Enter fullscreen mode Exit fullscreen mode

Notice something.

The View didn't disappear.

The Button didn't disappear.

The important change is the text.


Then: Commit

Yoga/layout work happens as needed.

The new tree becomes the next tree.


Finally: Mount

The renderer compares the previous rendered tree and the new tree.

Conceptually:

Previous
   ↓
Text = "0"

Next
   ↓
Text = "1"

Diff
   ↓
Update Text
Enter fullscreen mode Exit fullscreen mode

So the native UI can perform a small update rather than rebuilding the entire screen.

This is one of the most important ideas to remember:

React re-rendering does not mean the entire native UI is recreated.

The renderer determines what native mutations are actually necessary.

Shows count: 0 → count: 1, then the old/new trees, diff, and finally only the required native update.


Where Can Performance Problems Appear?

Understanding the rendering pipeline changes how I think about React Native performance.

Before learning this, it is easy to think:

"React Native performance means making JavaScript faster."

But that is only one part of the story.

There are multiple places where work can become expensive.


1. Too Much JavaScript Work

If your component logic is expensive, React may take longer to produce the next UI representation.

For example:

Large computation
     ↓
Slow render
     ↓
Delayed UI update
Enter fullscreen mode Exit fullscreen mode

This is why things like unnecessary calculations, unnecessary component updates, and poorly structured state can matter.


2. Too Much UI

Imagine rendering hundreds or thousands of complex components.

Even if your JavaScript is reasonable, the renderer still has more UI to reason about.

Large lists are a classic example.

This is why list virtualization and careful component design matter.


3. Complex Layout

A complicated UI can require significant layout calculation.

For example:

Nested containers
      ↓
Many layout constraints
      ↓
   More work
Enter fullscreen mode Exit fullscreen mode

Yoga is efficient, but it still has work to do.


4. Too Many Native Mutations

Eventually, changes have to reach native views.

If an update causes a large number of native operations:

createView
updateView
removeView
insertView
...
Enter fullscreen mode Exit fullscreen mode

that work can become expensive.

So performance isn't just about:

"How fast is my JavaScript?"

It is also about:

"How much work is required across the entire rendering pipeline?"


The Old Architecture vs The New Architecture

This is where many React Native discussions become confusing.

People often say:

"Old Architecture was Bridge and New Architecture is Fabric."

That's useful as a starting point, but it isn't the complete picture.

The New Architecture changed several major pieces of React Native's internals.

In the old architecture, React Native relied heavily on an asynchronous Bridge to serialize and enqueue communication between JavaScript and native code.

The New Architecture removes that dependency and introduces a new renderer, new native module/component systems, and better support for modern React capabilities.

A simplified comparison looks like this:

Area Legacy Architecture New Architecture
Renderer Legacy renderer Fabric
JS ↔ Native communication Asynchronous Bridge JSI-based interoperability
Rendering model Legacy rendering model Modern renderer
Native modules Legacy Native Modules New Native Module system
Native components Legacy system New Native Component system
React capabilities More limited by architecture Better support for modern React features
Core rendering logic More platform-specific More shared C++ logic

Starting with React Native 0.76, the New Architecture became enabled by default and was declared ready for production use.

But there is an important detail.

The New Architecture is not a magic switch that automatically makes every application fast.

Your application can still have:

Bad state management
      ↓
Unnecessary renders
      ↓
Heavy computations
      ↓
Huge lists
      ↓
Expensive UI
Enter fullscreen mode Exit fullscreen mode

Changing the architecture does not remove application-level bottlenecks.

The architecture gives React Native better foundations and capabilities.

We still have to build our applications carefully.

Side-by-side comparison of Legacy Architecture / Bridge and New Architecture / Fabric / JSI, focusing on architectural differences rather than simply labeling one as


What About Threads?

This is another area where React Native rendering can become confusing.

You may hear things like:

JavaScript Thread
UI Thread
Background Thread
Enter fullscreen mode Exit fullscreen mode

And then try to memorize:

"Render always happens here, Commit always happens there, Mount always happens there."

That is not a good mental model.

The actual implementation and scheduling can vary.

React Native's rendering architecture involves JavaScript and C++ work, layout/commit work, and native UI work, with scheduling depending on the situation and platform.

So instead of memorizing:

Render = Thread X
Commit = Thread Y
Mount = Thread Z
Enter fullscreen mode Exit fullscreen mode

I prefer to remember the responsibility:

Render
→ What should the UI tree look like?

Commit
→ Prepare the next tree and calculate its layout.

Mount
→ Apply the necessary changes to native UI.
Enter fullscreen mode Exit fullscreen mode

That mental model survives implementation details much better.


Why Does the Shadow Tree Matter?

At this point, we can understand why the Shadow Tree exists.

Without an intermediate representation, the renderer would have to reason directly about platform-specific native views.

Instead, Fabric can work with a platform-independent representation:

React
  ↓
Shadow Tree
  ↓
Layout
  ↓
Diff
  ↓
Native Views
Enter fullscreen mode Exit fullscreen mode

This creates an important separation.

React doesn't need to know whether the final platform is:

Android
Enter fullscreen mode Exit fullscreen mode

or:

iOS
Enter fullscreen mode Exit fullscreen mode

The renderer and host platform implementation handle that part.

This is also one reason Fabric's shared C++ core is important.

More rendering logic can be shared across platforms instead of being duplicated in completely separate implementations.


Why Does Mounting Use a Diff?

Imagine an application with this UI:

Screen
 ├── Header
 ├── Profile
 ├── Transactions
 │    ├── Transaction 1
 │    ├── Transaction 2
 │    └── Transaction 3
 └── Footer
Enter fullscreen mode Exit fullscreen mode

Now imagine one transaction changes.

We don't want to blindly do:

Delete everything
Create everything again
Enter fullscreen mode Exit fullscreen mode

Instead:

Previous Tree
      +
  Next Tree
      ↓
   Compare
      ↓
  Find changes
      ↓
Apply mutations
Enter fullscreen mode Exit fullscreen mode

Maybe the result is simply:

Update Transaction 2
Enter fullscreen mode Exit fullscreen mode

The Mount phase turns the difference between trees into the required native mutations.

This is one of the places where the architecture connects directly to something we care about as developers:

efficient UI updates.


The Complete Mental Model

Let's zoom out.

We started with:

<View>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

But now we can see the bigger picture.

React Code
     │
     ▼
React Element Tree
     │
     ▼
Reconciliation
     │
     ▼
Fabric Renderer
     │
     ▼
Shadow Tree
     │
     ▼
Yoga Layout
     │
     ▼
   Commit
     │
     ▼
Previous Tree + Next Tree
     │
     ▼
    Diff
     │
     ▼
   Mount
     │
     ▼
Native Views
     │
     ▼
   Screen
Enter fullscreen mode Exit fullscreen mode

The Final Mental Model — A visual summary of the complete React Native rendering pipeline, showing how React Code → Reconciliation → Fabric → Shadow Tree → Yoga → Commit → Mount → Native UI → Screen fits into the three core phases: Render → Commit → Mount.

And the three phases can be remembered very simply:

RENDER

"What should the UI look like?"

COMMIT

"Prepare the next tree and calculate its layout."

MOUNT

"Apply the necessary changes to native UI."
Enter fullscreen mode Exit fullscreen mode

The Bigger Picture

When I first started working with React Native, I mostly thought about components.

<View />
<Text />
<FlatList />
<Pressable />
Enter fullscreen mode Exit fullscreen mode

Then I learned about state.

Then performance.

Then architecture.

But understanding the rendering pipeline added another layer to the picture.

A React Native application is not simply:

JavaScript → Screen
Enter fullscreen mode Exit fullscreen mode

There is a rendering system between those two worlds.

JavaScript
    ↓
  React
    ↓
  Fabric
    ↓
Shadow Tree
    ↓
  Layout
    ↓
  Commit
    ↓
  Mount
    ↓
Native UI
Enter fullscreen mode Exit fullscreen mode

And suddenly terms that initially sounded unrelated start to connect.

Reconciliation helps determine the new React result.

Fabric is the rendering system.

Shadow Tree represents the UI before it becomes native views.

Yoga calculates layout.

Commit prepares the next tree.

Mount applies the necessary changes to the native UI.

These aren't separate topics.

They are different parts of one process.


One Simple Way to Remember It

If I had to explain the entire rendering system in an interview without going too deep into implementation details, I would say:

"React Native takes the React component tree produced by JavaScript and uses its renderer, Fabric, to build a Shadow Tree. During the render phase, React and the renderer create the next UI representation. During commit, layout is calculated using Yoga and the new tree is prepared. During mount, React Native diffs the trees and applies the required mutations to the native views."

That explanation is much more useful than memorizing a list of terms.

Because now every term has a responsibility.

React
→ describes the UI

Reconciliation
→ determines the new React result

Fabric
→ rendering system

Shadow Tree
→ intermediate representation

Yoga
→ layout calculation

Commit
→ prepare the next tree

Mount
→ update native UI
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

The interesting part of React Native is that the code we write is often much simpler than the system executing it.

When we write:

<View>
  <Text>Hello</Text>
</View>
Enter fullscreen mode Exit fullscreen mode

we don't normally need to think about:

React Element Tree
Shadow Nodes
Shadow Tree
Yoga
Commit
Tree Diffing
Mount
Native Views
Enter fullscreen mode Exit fullscreen mode

And that's exactly what a good abstraction should do.

We can build applications without understanding every internal detail.

But when we start working on performance, debugging difficult rendering problems, building native components, or simply trying to understand why React Native works the way it does, these concepts become extremely useful.

For me, the biggest takeaway is not memorizing the names.

It is understanding the journey:

React describes the UI. Fabric builds a representation of that UI. Yoga calculates its layout. Commit prepares the next tree. Mount turns the result into native UI.

Once that mental model is clear, React Native rendering stops feeling like a collection of mysterious internal terms.

It becomes one connected system.

And that is the part worth remembering.


Buy Me a Coffee

Top comments (0)