How React Works Under the Hood
React may seem magical, but behind the scenes, it's a highly optimized and elegant system.
In this comprehensive deep dive, we’ll explore:
- What React DOM really does
- How JSX is parsed into JavaScript objects
- Why every component is just a function or object
- The diffing and reconciliation process
- How Hooks like
useState
anduseEffect
really work - Optimization strategies React uses
- React Fiber: the heart of the React rendering engine
- How to build your own mini React
Let’s decode the internals
1. React DOM: The Rendering Engine
React is a powerful JavaScript library for building user interfaces, but it doesn't interact directly with the browser's Document Object Model (DOM). That crucial responsibility is handled by ReactDOM, which serves as the rendering engine that connects React's declarative component model to the actual browser environment. When developers write a command like ReactDOM.render(<App />, document.getElementById('root'));
, they're instructing ReactDOM to take the virtual component tree and mount it into the specified location in the real DOM. Behind the scenes, ReactDOM creates a virtual representation
of the UI elements, compares them during updates using a diffing algorithm, and applies only the minimal set of changes needed to the real DOM — improving performance and responsiveness.
ReactDOM's strength lies in its ability to efficiently manage host environment logic, meaning it knows how to interact with browser-specific APIs like document.createElement
, appendChild
, or removeChild
. This separation of concerns allows React itself to remain platform-agnostic — enabling other renderers like React Native or React 360 — while ReactDOM is specifically tailored for web browsers. By handling DOM interactions, event delegation, and mounting lifecycle logic, ReactDOM plays a critical role in making React applications feel fast, interactive, and modern.
2. How JSX Works Internally
JSX may look like regular HTML, but it's actually a JavaScript syntax extension that simplifies how we write React components. Instead of manually calling JavaScript functions to create DOM elements, JSX allows us to write components in a familiar, HTML-like structure. But here's the catch: JSX doesn’t directly work in the browser. It’s converted into JavaScript before the browser can run it. JSX is essentially syntactic sugar, meaning it makes writing code easier, but it still gets converted into something else under the hood.
Let’s break it down with a simple example:
<div>Hello, world!</div>
At first glance, this looks like regular HTML. But when JSX is compiled by tools like Babel, it gets turned into a JavaScript function call like this:
React.createElement('div', null, 'Hello, world!');
This may seem confusing at first, but what’s happening here is that React.createElement is a function that React uses to create a virtual representation of the UI you’re building. In this case, it’s creating an object that describes the
element, its attributes (none in this case), and its content (the text 'Hello, world!').Why Do We Use JSX?
JSX makes React development more intuitive and less verbose. Imagine creating a list of items in pure JavaScript without JSX:
const list = React.createElement('ul', null,
React.createElement('li', null, 'Item 1'),
React.createElement('li', null, 'Item 2'),
React.createElement('li', null, 'Item 3')
);
This looks cumbersome compared to the JSX version:
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
As you can see, JSX provides a cleaner, more readable way to write React components. And under the hood, it’s just a simpler, more intuitive way to call React.createElement
.
3. Components Are Objects or Functions
In React, components are the building blocks of your application. A component is a reusable piece of code that defines part of the UI. You can think of a component as a function or a class, both of which return a description of what the UI should look like. This description is what React will use to create the actual user interface in the browser. So, when you create components, you’re basically writing a function or class that returns what the UI should look like at that moment.
Function Components
Let’s start with function components, which are simpler and more common in modern React development .A function component is just a function that returns JSX. Here’s an example:
function MyButton() {
return <button>Click me</button>;
}
In this example, MyButton is a function that returns a JSX element a <button> element with the text Click me
. This function component looks simple, but when it’s rendered, React does a lot behind the scenes. It takes this JSX, converts it into a virtual DOM node, and eventually updates the real DOM on the page.
Now, when you call the MyButton component in JSX like this:<MyButton>
React doesn’t directly render it to the page. Instead, it takes the JSX and creates a JavaScript object that represents the component. This object looks like this:
{
type: MyButton,
props: {}
}
This object has two important properties:
- type: This refers to the component itself (MyButton).
- props: This refers to the properties (or "props") passed to the component. In this case, there are no props passed, so it's an empty object.
How React Uses Components
React uses both function and class components in a similar way. Whether you define your component as a function or a class, React calls the component to get the description of the UI (the JSX), then builds a tree of virtual DOM nodes. This process is recursive — meaning if you have a component that contains other components, React will also create virtual DOM nodes for those components.
For example, let’s say you have a parent component and a child component:
function Parent() {
return <MyButton />;
}
Here, React will first create the virtual DOM node for the Parent component. Then, it will call the MyButton component inside the Parent component and create the virtual DOM node for it too. It does this recursively, meaning if components are nested, React will go through each one to build the complete tree of virtual DOM nodes.
4. Reconciliation and Virtual DOM
One of the main reasons React is known for its performance is its reconciliation algorithm and the use of the Virtual DOM. But what does this mean, and how does it help make React fast and efficient? Let's break it down step by step.
What is the Virtual DOM?
To understand how React updates the real DOM efficiently, we first need to understand the Virtual DOM. The Virtual DOM is a lightweight, in-memory representation of the real DOM. It’s essentially a copy of the UI, but it’s not the actual elements on the page. React uses this virtual representation to figure out what changes need to be made to the real DOM, without directly manipulating it right away. This saves a lot of time and resources because interacting with the real DOM can be slow, especially when there are many elements.
When State Changes
Now, let’s say something changes in your app, such as when a user clicks a button or enters text in an input field. This is a state change, and when state changes, React follows these steps:
- Rebuild the Virtual DOM: React re-renders the component and rebuilds the entire Virtual DOM tree. This means React creates a new in-memory copy of the UI based on the updated state.
- Compare the Virtual DOMs: React then compares the newly created Virtual DOM tree to the previous one. This comparison helps React figure out what has changed between the two.
- Apply Necessary Updates: After React has identified the differences, it applies only the necessary updates to the real DOM. React doesn’t update the entire real DOM — just the parts that have changed. This process is called reconciliation.
How Reconciliation Works
React's reconciliation process follows specific rules to make updates efficient. Let’s go through them one by one:
-
Elements of Different Types: If the type of the element has changed (for example, if a is replaced by a ), React will completely replace the existing DOM node with a new one. This ensures that React doesn’t try to keep any unnecessary information when the structure changes.
For example, if you change a <div> to a <button>:
<div>Click me</div> // Previous <button>Click me</button> // Updated
React will replace the entire <div> node with a new <button> node.
- Same Type: If the element type is the same (for example, a <div> to another <div>), React will update the existing element’s props and children. React only updates the things that need to be updated. This makes updates much faster than creating a whole new element.
For instance, if you change the text inside a <div>:
<div>Hello</div> // Previous <div>Goodbye</div> // Updated
React will just update the text inside the same <div> element, rather than creating a whole new element.
- Children Need Keys for List Diffing: When working with lists of elements (like in a list or a table), React needs a way to keep track of each item in the list when the list changes. For this, React uses keys. A key is a unique identifier that helps React match the old items in the list with the new ones.
For example, consider this list of items:
<ul> <li>Item 1</li> <li>Item 2</li> </ul> If you change the order or remove items from the list, React needs a way to keep track of which item is which. Without keys, React might get confused about which item should be updated and might not perform updates correctly. That’s why you should always provide a unique key for each item in a list:
- Item 1
- Item 2
By providing keys, React can efficiently update the list when changes occur, reducing unnecessary re-renders and keeping the UI in sync with the data.
5. Hooks Internals
React Hooks like useState, useEffect, and others may seem like magic at first, but under the hood, their logic is surprisingly elegant. Understanding how they work internally not only gives you more confidence while coding, but also helps avoid common mistakes like using hooks conditionally or inside loops.
Let’s break it down step by step.
The Concept: Hooks as State Slots
When you use hooks like useState, React manages state without classes. Instead, it internally keeps track of an array of state values (or “slots”) for each functional component. Each time your component renders, React walks through that array, calling your hooks in the exact same order.
Here’s a simplified version of how useState might work behind the scenes:
let hookIndex = 0; const hooks = []; function useState(initial) { const currentHook = hooks[hookIndex] ?? initial; const setState = (newValue) => { hooks[hookIndex] = newValue; rerender(); // Re-render the component when state changes }; hookIndex++; return [currentHook, setState]; }
This example assumes a global hookIndex and hooks array. Every time the component renders, hookIndex resets to 0, and React walks through the hooks in the same order they were declared. When you call useState(), it looks at the hooks[0] slot on the first call, then hooks[1] on the second call, and so on.
Why Hooks Must Be Called in the Same Order
Because React relies on the order of hook calls to match values with the correct state slot, you must never call hooks conditionally or inside loops. Otherwise, the order will be inconsistent between renders, and React won’t know which slot corresponds to which hook, causing bugs or errors.Incorrect (don’t do this):
function MyComponent(props) { if (props.show) { const [count, setCount] = useState(0); // ❌ Hook inside a condition } }
Correct:
function MyComponent(props) { const [count, setCount] = useState(0); // ✅ Always called // You can conditionally use the value later }
React needs this consistent ordering to work properly — think of it like stepping through a list of saved values. If the order changes, React ends up reading or writing to the wrong index.
Internal Flow of Hooks in a Component
Imagine a component like this:
function Counter() { const [count, setCount] = useState(0); const [name, setName] = useState('Guest'); return ( <div> <p>{name}: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
Internally, React builds a hook state array something like:
hooks = [0, 'Guest']; // index 0 = count, index 1 = name
When you click the button, setCount updates index 0 and triggers a re-render. During the re-render, React reads from the hooks array in the same order to re-apply the useState calls.
6. React Fiber: The Core Architecture
What is Fiber?
React Fiber is the engine that powers React behind the scenes. Introduced in React 16, Fiber was a complete rewrite of React's internal rendering logic to improve performance, responsiveness, and flexibility.
Before Fiber, React used a synchronous, blocking update model — which meant large updates could freeze the UI. Fiber changed all that by making rendering incremental and interruptible.
Think of Fiber like a highly efficient project manager. Instead of doing all tasks at once (which might delay user interactions), it breaks them into smaller chunks and pauses/reorders them based on priority.
How Does Fiber Work?
Fiber breaks down rendering work into units, and schedules them based on priority. This makes React feel faster and more responsive — especially on slow devices or large apps.
Here’s how Fiber improves the update process:
Incremental rendering: React doesn’t process the whole component tree in one go. It can pause work and resume it later.
Interruptible rendering: If a more urgent update (like a user typing) comes in, React can pause current work and handle the urgent update first.
Smart scheduling: React uses a scheduler that decides what updates to do now, and what can wait (e.g., transitions or animations).
The Fiber Node
Each React element or component becomes a Fiber node — a JavaScript object with links to related nodes and properties. Imagine it like a tree where each node is a task with connections to siblings, parents, and children.
Here’s what a simplified Fiber node looks like:
{ type: MyComponent, // Function or DOM type child: [childNode], // First child in the tree sibling: [nextSibling], // Next sibling on the same level return: [parentNode], // Parent fiber props: { ... }, // Props passed to the component stateNode: DOM or class // Reference to DOM node or class instance alternate: [oldFiber] // Used for diffing with previous version } This forms a linked list/tree structure, allowing React to efficiently walk through and update parts of the UI.
Scheduling and Priorities
React assigns each update a priority level, called a lane. For example:
- urgent: typing input, clicks
- default: UI updates
- transition: animations or routes
- idle: low-priority background tasks
React uses a loop (like requestIdleCallback) to decide which updates to work on, and can yield (pause) between chunks so it doesn’t block the browser.
Think of it like juggling — React keeps many tasks in the air, but it knows which ones to catch first based on importance.
The Commit Phase
Once React finishes building the updated Fiber tree (this is called the render phase), it enters the commit phase, where:
- DOM updates are applied (in a batch).
- Effects like useLayoutEffect and useEffect are executed.
By separating the render and commit phases, React ensures smooth visual updates and allows powerful features like concurrent rendering, Suspense, and transitions.
React Fiber is the heart of modern React, enabling:
- Smooth, responsive interfaces
- The ability to pause and resume rendering
- Smarter prioritization of UI tasks
This internal architecture makes it possible to build complex apps without worrying about performance — React takes care of the heavy lifting behind the scenes.
Conclusion
Diving into React's internals isn’t just for advanced developers — it’s a powerful way to level up your day-to-day React skills. When you understand how React works under the hood — from JSX and component trees to the Fiber architecture and hooks — everything starts to make more sense.
You begin to write cleaner code, avoid common pitfalls, and debug with confidence because you know why React behaves the way it does. You realize that things like stable hook order, using keys in lists, or why rendering is fast and batched aren’t just “rules” — they’re design decisions based on a deeply thought-out engine.
React is not just a library — it’s a state machine, a scheduler, and a rendering engine all working in sync to deliver fluid user experiences. And the best part? It gives you a simple, declarative API to work with, so you can focus on your product while it handles the complexity.
So, the next time something in your app feels "off," or you want to improve performance, remember: React’s inner workings are not a black box.
Keep Learning , Keep Growing
Top comments (2)
Just started learning react and this is so helpful 💡 Thanks for sharing ✨️
I am glad that it is helpful. You can follow me for upcoming insightful articles .