How React Works
Introduction
Hey everyone!
In this blog, we’re going to understand how React actually works. What happens between you writing a component and it showing up on the screen?
If you’re completely new to this, my suggestion is not to overwhelm yourself with too much information. Understand what you can and move ahead (let the future you suffer 😉)
What we are going to cover:
- Mounting
- Updating
- Unmounting
So let’s understand this using a simple example.
Let’s assume we have this counter code written in our app:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}On running this, we get this screen:
Now, what happens when I click the “Increment” button? Let’s check! The count goes from 0 to 1.
Basically, one click updates the value of count by 1. But behind that one click, React does quite a bit of work 😮💨
Today, we’ll learn how React actually renders this.
Before anything runs: browsers don’t understand JSX. So when you build your app, your build tool (like Babel or SWC) converts it into normal JavaScript. This happens once, before the app runs, so it’s not part of the steps below.
// What you write
<h1>Count: {count}</h1>// What it becomes (roughly)
jsxs("h1", { children: ["Count: ", count] })Step 1: Mounting
Mounting is when React shows a component on the screen for the very first time.
Let’s see what happens, step by step.
1. JS becomes React element objects. When React calls your Counter function, those calls return plain JavaScript objects called React elements. Something like this:
{
type: "h1",
props: {
children: ["Count: ", 0]
}
}So simply, a React element is just an object describing what you want on the screen 🙂
2. React elements become DOM. React reads these objects, creates real DOM elements (<h1>, <button>) and inserts them into the page.
3. The browser paints. The browser uses that DOM to paint the pixels, and you see the webpage.
4. Effects run. After the page is painted, React runs your useEffect callbacks. (useLayoutEffect is the exception: it runs right after the DOM is updated, before the paint.)
So basically, when mounting, React:
- Runs the code you wrote (render phase)
- Creates and inserts DOM elements (commit phase)
- Executes callbacks like
useEffect
One more thing: after mounting, React keeps this tree of element objects in memory. We’ll need it in the next step.
Step 2: Updating
Updating happens when a component’s state changes, its parent re-renders, or a context it uses changes. Then React has to show the new data on the screen.
In our example, let’s check what happens when you click “Increment”.
1. The click schedules an update. Your onClick runs setCount(count + 1). React doesn't change count right away. It queues the new value (1) and schedules a re-render for later.
Get Bhavesh Anandpara’s stories in your inbox
Join Medium for free to get updates from this writer.
Yes, you read that right 🤯 Calling setCount does not update the screen immediately. It just tells React "hey, this needs an update", and React does it a moment later.
Don’t believe me? Let’s check! Add a console.log right after it:
<button onClick={() => {
setCount(count + 1);
console.log(count); // prints 0, not 1!
}}>Increment</button>See! Inside this click, count is still 0. The new value shows up only in the next render.
2. React calls Counter again (render phase). This time useState returns 1. Your code runs just like before and gives a new tree of React elements:
{
type: "h1",
props: {
children: ["Count: ", 1]
}
}3. React compares the new tree with the old one. Remember the tree React kept in memory after mounting? React compares the new elements with those old ones. This is called reconciliation. We’ll dig deeper into this in the next blog. Here, the only change is 0 → 1.
4. React updates only what changed (commit phase). React doesn’t rebuild the page. It reuses the existing <h1> and changes just its text from 0 to 1. The <button> isn't touched.
5. The browser paints. You see “Count: 1”.
6. Effects re-run. If a useEffect depends on count, React first runs its cleanup from the previous render, then runs the effect again with the new value. (Our counter has no useEffect, so nothing runs here.)
So simply, updating = new state → render again → compare → change only what’s different ✨
Step 3: Unmounting
Unmounting is when React removes a component from the screen because we don’t want to show it anymore.
So let’s understand this using an example. We add a toggle button in App.jsx:
import { useState } from "react";
import Counter from "./Counter";
export default function App() {
const [showCounter, setShowCounter] = useState(true);
return (
<div>
<button onClick={() => setShowCounter(!showCounter)}>
Toggle counter
</button>
{showCounter && <Counter />}
</div>
);
}When you click “Toggle counter”, showCounter becomes false. Now App no longer returns <Counter />, so React never calls Counter() again.
Let’s see what happens, step by step.
1. The click schedules an update. Same as before, setShowCounter(false) is scheduled for later.
2. React calls App again (render phase). This time the new tree has no <Counter /> in it.
3. React compares the new tree with the old one. It sees that Counter was there before and is gone now. So it has to be removed.
4. React removes the DOM (commit phase). React removes the counter’s <h1> and <button> from the page. (If the component had useLayoutEffect cleanups or refs, those are cleaned up right here too.)
5. The browser paints. The counter is gone from the screen.
6. Cleanups run. After the commit, React runs the cleanup functions of the counter’s useEffects. This usually happens after the paint. (When the update comes from a click, like ours, React may run them a bit earlier, before the paint.)
If something else takes its place, like {showCounter ? <Counter /> : <p>Counter hidden</p>}, that new <p> gets mounted just like Step 1.
Why do cleanups matter? Imagine our counter had a timer:
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id); // cleanup
}, []);When Counter unmounts, React runs clearInterval(id) for you. Without that cleanup, the timer keeps ticking even after the counter is gone 😮💨
One more surprise: unmounting also throws away the component’s state. Toggle the counter off and on again, and the count starts from 0.
Unmounting in one line: remove from the tree → remove from the DOM → clean up.
Here’s the whole process
Read each column top to bottom. Every stage goes through the same phases: trigger → render → commit → paint → effects. Mounting happens once, updating repeats on every state change, and unmounting happens once at the end ✌️
Tip
Re-render ≠ DOM update. When a component re-renders, React re-runs your function, but it only touches the DOM where something actually changed. Keep this one in mind, it comes back when we talk about performance.
That’s all for today, folks. Catch up with this series later for more updates on React from your host and dost — Bhavesh
If you found this helpful, feel free to like it and share your thoughts in the comments! ✨
What’s next?
Next up, we’ll dive into the Virtual DOM, Reconciliation and Component Identity. That’s where all of this clicks.









