Episode 2 — React Frontend Architecture NextJS / 2.1 — Introduction to React
2.1.a — Why React Exists
In one sentence: React was created by Facebook to solve the pain of building large-scale, data-driven user interfaces where manual DOM manipulation becomes unmanageable, error-prone, and slow.
Navigation: ← Overview · Next → Declarative vs Imperative UI
Table of Contents
- The Problem That Started It All
- Manual DOM Manipulation at Scale
- The jQuery Era — What Went Wrong
- Facebook's Specific Pain Point
- The Birth of React — 2013
- What React Actually Is
- React's Core Philosophy
- How React Solves the DOM Problem
- React vs Vanilla JavaScript — A Real Comparison
- Why Not Just Use Templates?
- React's Ecosystem and Popularity
- When React Is NOT the Right Choice
- React's Evolution — From Classes to Hooks
- The Mental Model Shift
- Key Takeaways
1. The Problem That Started It All
Before we write a single line of React code, we need to understand why it exists. Every technology is born from a problem. If you don't understand the problem, you'll never truly understand the solution.
The Early Web (1995–2005)
In the beginning, websites were static HTML documents. A server generated the full HTML page, sent it to the browser, and the browser displayed it. If you wanted to change anything on the page — even a single character — the entire page had to reload.
┌──────────┐ Request ┌──────────┐
│ │ ─────────────▶ │ │
│ Browser │ │ Server │
│ │ ◀───────────── │ │
└──────────┘ Full HTML └──────────┘
Page
User clicks a link → FULL PAGE RELOAD
This worked fine for simple websites — news articles, personal homepages, online encyclopedias. But as the web grew more interactive, this model broke down.
The Rise of Dynamic Interfaces
By the mid-2000s, web applications were becoming more ambitious:
- Gmail (2004) — real-time email updates without page reload
- Google Maps (2005) — drag, zoom, and search without losing your position
- Facebook (2004+) — news feed, notifications, chat, all updating in real-time
These apps needed to change parts of the page without reloading the whole thing. This is where JavaScript DOM manipulation entered the picture.
What Is the DOM?
The DOM (Document Object Model) is the browser's representation of your HTML page as a tree of objects. Every HTML element becomes a JavaScript object that you can read, modify, or delete.
HTML:
<div id="app">
<h1>Hello</h1>
<p>World</p>
</div>
DOM Tree:
document
│
<html>
│
<body>
│
<div#app>
/ \
<h1> <p>
│ │
"Hello" "World"
JavaScript can manipulate this tree directly:
// Find an element
const heading = document.getElementById('app').querySelector('h1');
// Change its content
heading.textContent = 'Goodbye';
// Add a new element
const newParagraph = document.createElement('p');
newParagraph.textContent = 'New content';
document.getElementById('app').appendChild(newParagraph);
// Remove an element
const oldParagraph = document.querySelector('p');
oldParagraph.remove();
// Change styles
heading.style.color = 'red';
heading.style.fontSize = '24px';
// Add event listeners
heading.addEventListener('click', () => {
alert('You clicked the heading!');
});
This seems simple enough. So what's the problem?
2. Manual DOM Manipulation at Scale
The problem isn't that DOM manipulation is hard to write. The problem is that it becomes impossible to manage as your application grows.
A Simple Counter — Vanilla JS
Let's build a counter with vanilla JavaScript:
<div id="counter-app">
<h2>Count: 0</h2>
<button id="increment">+1</button>
<button id="decrement">-1</button>
<button id="reset">Reset</button>
</div>
<script>
let count = 0;
const display = document.querySelector('#counter-app h2');
document.getElementById('increment').addEventListener('click', () => {
count++;
display.textContent = `Count: ${count}`;
});
document.getElementById('decrement').addEventListener('click', () => {
count--;
display.textContent = `Count: ${count}`;
});
document.getElementById('reset').addEventListener('click', () => {
count = 0;
display.textContent = `Count: ${count}`;
});
</script>
That's about 20 lines of JavaScript for a counter. Manageable. Now let's add requirements:
- Show "Positive", "Negative", or "Zero" below the count
- Change the color to green (positive), red (negative), or gray (zero)
- Disable the decrement button when count is 0
- Show a history of the last 5 changes
<div id="counter-app">
<h2 id="count-display">Count: 0</h2>
<p id="status" style="color: gray">Zero</p>
<button id="increment">+1</button>
<button id="decrement" disabled>-1</button>
<button id="reset">Reset</button>
<h3>History</h3>
<ul id="history"></ul>
</div>
<script>
let count = 0;
let history = [];
const countDisplay = document.getElementById('count-display');
const status = document.getElementById('status');
const decrementBtn = document.getElementById('decrement');
const historyList = document.getElementById('history');
function updateUI() {
// Update count display
countDisplay.textContent = `Count: ${count}`;
// Update status text and color
if (count > 0) {
status.textContent = 'Positive';
status.style.color = 'green';
} else if (count < 0) {
status.textContent = 'Negative';
status.style.color = 'red';
} else {
status.textContent = 'Zero';
status.style.color = 'gray';
}
// Update button state
decrementBtn.disabled = count === 0;
// Update history
historyList.innerHTML = '';
history.slice(-5).forEach(entry => {
const li = document.createElement('li');
li.textContent = entry;
historyList.appendChild(li);
});
}
document.getElementById('increment').addEventListener('click', () => {
count++;
history.push(`Incremented to ${count}`);
updateUI();
});
document.getElementById('decrement').addEventListener('click', () => {
if (count === 0) return;
count--;
history.push(`Decremented to ${count}`);
updateUI();
});
document.getElementById('reset').addEventListener('click', () => {
count = 0;
history.push('Reset to 0');
updateUI();
});
updateUI(); // initial render
</script>
We're now at 50+ lines for a counter. Notice the pattern:
- Data exists in JavaScript variables (
count,history) - UI exists in the DOM (
countDisplay,status,historyList) - Every time data changes, we manually update EVERY piece of the DOM that depends on it
This is the core problem: keeping the data (state) and the UI (DOM) in sync.
The Synchronization Nightmare
┌─────────────────────────────────────────────────────┐
│ THE MANUAL DOM PROBLEM │
├─────────────────────────────────────────────────────┤
│ │
│ STATE (JavaScript) DOM (Browser) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ count = 5 │ ──sync──▶ │ "Count: 5" │ │
│ │ status = │ ──sync──▶ │ "Positive" │ │
│ │ "Positive" │ │ color:green │ │
│ │ history = [ │ ──sync──▶ │ <li>...</li>│ │
│ │ ... │ │ <li>...</li>│ │
│ │ ] │ │ <li>...</li>│ │
│ │ disabled = │ ──sync──▶ │ btn.disabled│ │
│ │ false │ │ = false │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ YOU are responsible for EVERY arrow. │
│ Miss one? Bug. Wrong order? Bug. Race condition? │
│ Bug. Memory leak from old listeners? Bug. │
│ │
└─────────────────────────────────────────────────────┘
Now imagine this at the scale of Facebook's news feed:
- Hundreds of posts, each with like counts, comment counts, share buttons
- Real-time notifications updating in the header
- Chat messages coming in at the sidebar
- Friend suggestions updating
- Ad content rotating
- All of these interacting with each other
Manual DOM synchronization at this scale is essentially impossible without bugs.
The Five Horsemen of Manual DOM
| Problem | Description | Example |
|---|---|---|
| State-UI Desync | DOM doesn't reflect current data | Like count shows 5 but state says 6 |
| Spaghetti Updates | Every data change touches multiple DOM nodes | Changing user name requires updating header, sidebar, comments |
| Memory Leaks | Event listeners and references not cleaned up | Old list items still holding references after removal |
| Inconsistent State | Different parts of UI show different states | Chat shows "online" but header shows "offline" |
| Performance Bottlenecks | Touching DOM is expensive, bulk updates cause reflows | Updating 100 list items triggers 100 layout recalculations |
Why DOM Operations Are Slow
The browser DOM is not a simple JavaScript object. Each DOM operation triggers a cascade:
┌─────────────────────────────────────────────────────┐
│ WHAT HAPPENS WHEN YOU TOUCH THE DOM │
├─────────────────────────────────────────────────────┤
│ │
│ element.style.width = '200px'; │
│ │
│ 1. Browser receives the change │
│ 2. Recalculate styles (CSS cascade) │
│ 3. Layout / Reflow (compute positions & sizes) │
│ 4. Paint (fill in pixels) │
│ 5. Composite (combine layers) │
│ │
│ Steps 2-5 may cascade: one change can force │
│ ALL sibling/child elements to recalculate too. │
│ │
│ Do this 100 times in a loop = 100 reflows. │
│ The page visibly stutters and hangs. │
│ │
└─────────────────────────────────────────────────────┘
A single style change can trigger a "reflow" — the browser recalculates the position and size of every affected element on the page. Batch many changes together without careful management, and the browser spends more time recalculating layouts than actually displaying your content.
3. The jQuery Era — What Went Wrong
jQuery (2006) was the first major attempt to make DOM manipulation easier. It simplified the API and handled browser inconsistencies:
// Vanilla JS (pre-jQuery, cross-browser nightmares)
var el;
if (document.getElementById) {
el = document.getElementById('myDiv');
} else if (document.all) {
el = document.all['myDiv']; // IE5
}
if (el.addEventListener) {
el.addEventListener('click', handler);
} else if (el.attachEvent) {
el.attachEvent('onclick', handler); // IE8
}
// jQuery — one line, all browsers
$('#myDiv').click(handler);
jQuery made DOM manipulation easier to write but didn't solve the fundamental problem — you were still manually keeping state and UI in sync.
jQuery's Real Contribution
jQuery deserves credit for:
- Normalizing browser APIs — Write once, run on IE6, Firefox, Chrome, Safari
- Simplifying DOM traversal — CSS selectors to find elements
- AJAX made easy —
$.ajax()simplified network requests - Plugin ecosystem — Thousands of UI components
- Teaching a generation — jQuery taught millions of developers JavaScript
Where jQuery Failed at Scale
// A real-world jQuery notification system (simplified)
$(document).ready(function() {
var unreadCount = 0;
var notifications = [];
function fetchNotifications() {
$.ajax({
url: '/api/notifications',
success: function(data) {
notifications = data;
unreadCount = data.filter(function(n) { return !n.read; }).length;
// Update badge in header
if (unreadCount > 0) {
$('#notification-badge').text(unreadCount).show();
} else {
$('#notification-badge').hide();
}
// Update notification dropdown list
$('#notification-list').empty();
data.forEach(function(notification) {
var $item = $('<li>')
.addClass(notification.read ? 'read' : 'unread')
.text(notification.message)
.click(function() {
markAsRead(notification.id);
$(this).removeClass('unread').addClass('read');
unreadCount--;
// MUST update badge here too — easy to forget!
if (unreadCount > 0) {
$('#notification-badge').text(unreadCount);
} else {
$('#notification-badge').hide();
}
// MUST update page title too — very easy to forget!
document.title = unreadCount > 0
? '(' + unreadCount + ') MyApp'
: 'MyApp';
});
$('#notification-list').append($item);
});
// Update page title
document.title = unreadCount > 0
? '(' + unreadCount + ') MyApp'
: 'MyApp';
// Update mobile nav badge (another place to forget!)
if (unreadCount > 0) {
$('#mobile-badge').text(unreadCount).show();
} else {
$('#mobile-badge').hide();
}
}
});
}
function markAsRead(id) {
$.post('/api/notifications/' + id + '/read');
}
setInterval(fetchNotifications, 30000);
fetchNotifications();
});
Count the number of places where unreadCount is used to update the DOM:
#notification-badgetext#notification-badgeshow/hide- Page title
#mobile-badgetext#mobile-badgeshow/hide
Each click handler must update ALL of these. Forget one, and you have a bug. Add a new UI element that shows the count (e.g., a desktop notification) and you have to find every place that modifies unreadCount and add the new update.
What Developers Needed
INSTEAD OF:
1. Something happened (user clicked, data arrived)
2. Figure out what DOM nodes need to change
3. Manually update each one
4. Hope you didn't forget any
5. Hope the order was right
6. Hope no other code conflicts
DEVELOPERS WANTED:
1. Something happened
2. Update the DATA
3. UI automatically reflects the new data
4. Done — no manual DOM touching
4. Facebook's Specific Pain Point
The Notification Bug
In 2011-2012, Facebook had a persistent bug that became legendary in frontend circles. Users would see a notification badge showing "1 unread message" in the header. They'd click it, read the message, but the badge wouldn't clear. Sometimes it would come back after navigating away and returning.
This wasn't a simple bug — it was a systemic problem. Multiple teams managed different parts of the Facebook UI:
- One team managed the chat sidebar
- Another managed the notification dropdown
- Another managed the top bar badges
- Another managed the message page
- Another managed the mobile web version
All of these had to stay in sync about the same underlying data: "How many unread messages does this user have?"
┌────────────────────────────────────────────┐
│ Facebook UI (2012) │
├────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────┐ │
│ │ Header: Messages (1) ← Team A │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Feed │ │ Chat Sidebar │ │
│ │ ← Team B │ │ New msg! ← Team │ │
│ │ │ │ C │ │
│ │ │ │ │ │
│ └──────────────┘ └──────────────────┘ │
│ │
│ Each team updates UI independently. │
│ They share DATA but not UPDATE LOGIC. │
│ Result: inconsistent UI everywhere. │
│ │
└────────────────────────────────────────────┘
What Made It So Hard to Fix
The problem wasn't that the developers were bad — it was that the architecture made consistency impossible:
- No single source of truth — Each component had its own copy of the notification count
- Multiple update paths — The count could change from API polling, WebSocket messages, user actions, or navigation
- No automatic propagation — When one component updated the count, other components didn't know
- Race conditions — Multiple async operations could conflict with each other
Jordan Walke's Insight
Jordan Walke, a Facebook engineer, realized the core problem: the UI was too far from the data.
In traditional approaches, the DOM was the source of truth. You'd read from the DOM, modify the DOM, and hope everything stayed consistent. Jordan asked: What if we flipped this?
What if:
- Data (JavaScript objects) is the single source of truth
- The UI is a pure function that transforms data into DOM elements
- Whenever data changes, you just re-run the function
- A smart system figures out what actually changed in the DOM
// The conceptual model Jordan envisioned:
function renderApp(data) {
return {
header: {
badge: data.unreadCount > 0 ? data.unreadCount : null
},
chatSidebar: {
messages: data.recentMessages
},
feed: {
posts: data.feedPosts.map(post => ({
...post,
likeCount: data.likes[post.id]
}))
}
};
}
// When data changes — just call renderApp() again.
// The framework diff's the old result with the new one.
// Only the actual DOM changes get applied.
This was the seed of React. The entire UI is re-described from scratch on every change, and a reconciliation algorithm figures out the minimal set of DOM operations needed.
From FaxJS to React
Jordan Walke's first implementation was called FaxJS (2011). It was a proof of concept that demonstrated the declarative re-rendering approach. Key experiments:
- Could you describe UI declaratively and have it work efficiently?
- Could you diff two UI descriptions to find minimal changes?
- Could this approach scale to Facebook's complexity?
The answer to all three was yes. FaxJS evolved into React, was deployed on Facebook's News Feed in 2012, then on Instagram, and was open-sourced in May 2013 at JSConf US.
5. The Birth of React — 2013
Timeline
| Year | Event |
|---|---|
| 2011 | Jordan Walke creates FaxJS, an early prototype |
| 2012 | FaxJS evolves into React, used internally at Facebook |
| 2012 | React powers Facebook's News Feed and Instagram |
| 2013 | React open-sourced at JSConf US (May 29, 2013) |
| 2014 | React Developer Tools released |
| 2015 | React Native announced (mobile apps with React) |
| 2015 | Redux introduced (state management companion) |
| 2016 | React 15 — major performance improvements |
| 2017 | React 16 — Fiber architecture (complete internal rewrite) |
| 2018 | React 16.6 — React.lazy, React.memo, Suspense |
| 2019 | React 16.8 — Hooks introduced (paradigm shift) |
| 2020 | React 17 — No new features, gradual upgrade support |
| 2022 | React 18 — Concurrent features, automatic batching, Suspense for data |
| 2024 | React 19 — Server Components stable, Actions, new hooks |
Initial Reception
React was controversial when it launched. The JavaScript community had two major objections:
Objection 1: "HTML in JavaScript?!"
// This horrified developers in 2013
function Button() {
return <button className="btn">Click me</button>;
}
Developers had spent years learning "separation of concerns" — HTML in .html files, CSS in .css files, JavaScript in .js files. JSX seemed to violate this principle.
React's counter-argument: The real separation of concerns isn't by technology (HTML/CSS/JS), but by component (Button, Header, Modal). A button's structure, style, and behavior are inherently coupled — separating them into three files doesn't reduce complexity, it distributes it.
Objection 2: "Re-render everything?!"
// Wasn't this wasteful?
// State changes → entire component re-renders → new JSX → React updates DOM
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Header /> {/* Re-renders even though nothing changed? */}
<Counter count={count} />
<Footer /> {/* Re-renders even though nothing changed? */}
</div>
);
}
The Virtual DOM diffing algorithm made this surprisingly efficient. React doesn't re-render the actual DOM — it re-renders a lightweight JavaScript representation, diffs it against the previous one, and applies only the changes.
Why React Won Despite the Controversy
React won because it solved real problems that developers faced daily. The initial resistance faded as teams saw:
- Fewer bugs — single source of truth eliminated state-UI desync
- Faster development — components are reusable across the app
- Easier debugging — one-way data flow makes bugs traceable
- Better onboarding — new team members can understand isolated components without understanding the whole app
6. What React Actually Is
React is a JavaScript library for building user interfaces. Let's break down what each word means:
"Library" — Not a Framework
This distinction matters. A framework controls the flow of your application — it calls your code. A library is called by your code — you control the flow.
┌──────────────────────────────────────────────┐
│ FRAMEWORK vs LIBRARY │
├──────────────────────────────────────────────┤
│ │
│ FRAMEWORK (Angular, Next.js): │
│ ┌──────────────────────────────┐ │
│ │ Routing ✓ │ │
│ │ HTTP client ✓ │ │
│ │ State management ✓ │ │
│ │ Form handling ✓ │ │
│ │ Testing utilities ✓ │ │
│ │ Build system ✓ │ │
│ │ UI rendering ✓ │ │
│ │ Dependency injection ✓ │ │
│ └──────────────────────────────┘ │
│ "We decide how your app is structured." │
│ "Your code plugs into our system." │
│ │
│ LIBRARY (React): │
│ ┌──────────────────────────────┐ │
│ │ UI rendering ✓ │ │
│ │ Everything else: your choice │ │
│ └──────────────────────────────┘ │
│ "We handle rendering. You decide the rest." │
│ "You call us when you need UI." │
│ │
└──────────────────────────────────────────────┘
Practical implication: React doesn't have opinions about:
- How you fetch data (fetch, axios, TanStack Query — your choice)
- How you handle routing (React Router, TanStack Router — your choice)
- How you manage global state (Context, Zustand, Redux — your choice)
- How you style components (CSS, Tailwind, styled-components — your choice)
- How you structure your folders (your choice entirely)
Pro: Freedom to choose the best tool for each job. Con: Decision fatigue — you have to choose everything yourself.
This is why Next.js exists (Topic 2.18) — it wraps React with opinionated defaults for routing, rendering, and more, turning the library into a full framework.
"User Interfaces" — Not the Whole App
React handles the view layer — what the user sees and interacts with. It doesn't handle:
- Data layer — How data gets to the browser (API calls, databases)
- Navigation — How URLs map to pages (routing)
- Business logic — Application rules and workflows
- Infrastructure — Build, deployment, hosting
React's Actual Public API
React's API is surprisingly small. The entire library comes down to a few core concepts:
// 1. CREATE ELEMENTS
// Raw API (you rarely use this directly):
React.createElement('div', { className: 'card' }, 'Hello');
// JSX (syntactic sugar — what you actually write):
<div className="card">Hello</div>
// 2. CREATE COMPONENTS (functions that return elements)
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// 3. MANAGE STATE (data that changes over time)
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
// 4. HANDLE SIDE EFFECTS (things outside React's control)
useEffect(() => {
document.title = `Count: ${count}`;
return () => { /* cleanup */ };
}, [count]);
// 5. RENDER TO THE DOM (entry point — called once)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
That's essentially it. Everything in the React ecosystem is built on top of these five concepts.
7. React's Core Philosophy
React is built on a few fundamental ideas that guide every design decision:
Philosophy 1: UI as a Function of State
This is the most important concept in React. If you remember one thing from this entire episode:
UI = f(state)
Your user interface is a pure function of your application's state (data). Given the same state, the function always produces the same UI output.
// state:
const state = {
todos: [
{ id: 1, text: 'Learn React', completed: true },
{ id: 2, text: 'Build a project', completed: false },
],
filter: 'active'
};
// f (the component):
function TodoList({ todos, filter }) {
const visible = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<ul>
{visible.map(todo => (
<li key={todo.id} className={todo.completed ? 'done' : ''}>
{todo.text}
</li>
))}
</ul>
);
}
// UI (the output):
// <ul>
// <li>Build a project</li>
// </ul>
Why this matters:
| With manual DOM | With React |
|---|---|
| UI is a side effect of imperative commands | UI is a return value of a function |
| Hard to predict what UI looks like for given data | Easy — just call the function with that data |
| Bugs from forgetting to update some DOM node | Can't happen — entire UI re-derived from state |
| Testing requires DOM simulation | Testing is just "does this function return the right JSX?" |
Philosophy 2: Composition Over Inheritance
In object-oriented programming, you often reuse code through inheritance — creating subclasses that extend base classes. React rejects this approach entirely for UI building.
Instead, you build complex UIs by composing simple components together:
// Small, focused, single-purpose components
function Avatar({ src, alt, size = 40 }) {
return (
<img
className="avatar"
src={src}
alt={alt}
width={size}
height={size}
/>
);
}
function UserName({ name, isOnline }) {
return (
<span className="username">
{name}
{isOnline && <span className="online-dot" />}
</span>
);
}
function Timestamp({ date }) {
const formatted = new Date(date).toLocaleDateString('en-US', {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
});
return <time className="timestamp">{formatted}</time>;
}
// Composed into a medium component
function CommentHeader({ author, date }) {
return (
<div className="comment-header">
<Avatar src={author.avatarUrl} alt={author.name} />
<UserName name={author.name} isOnline={author.isOnline} />
<Timestamp date={date} />
</div>
);
}
// Composed into a larger component
function Comment({ author, date, text, replies }) {
return (
<article className="comment">
<CommentHeader author={author} date={date} />
<p className="comment-body">{text}</p>
{replies.length > 0 && (
<div className="replies">
{replies.map(reply => (
<Comment key={reply.id} {...reply} />
))}
</div>
)}
</article>
);
}
This is like building with LEGO blocks — each piece is simple, but you can combine them into anything.
Philosophy 3: Unidirectional Data Flow
Data in React flows in one direction — from parent to child. Events (user actions) flow in the opposite direction — from child to parent.
┌──────────────────────────────────────────┐
│ UNIDIRECTIONAL DATA FLOW │
├──────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ App │ ← state lives here │
│ └─────┬────┘ │
│ data ↓ ↓ data │
│ ┌───────┐ ┌───────┐ │
│ │Header │ │ Main │ │
│ └───────┘ └───┬───┘ │
│ data ↓ ↓ data │
│ ┌───────┐ ┌───────┐ │
│ │ List │ │Detail │ │
│ └───────┘ └───────┘ │
│ │
│ DATA: Parent ──▶ Child (props) │
│ EVENTS: Child ──▶ Parent (callbacks) │
│ │
│ Child needs to change data? │
│ 1. Parent passes callback function │
│ 2. Child calls the callback │
│ 3. Parent updates its state │
│ 4. New data flows down to all children │
│ │
└──────────────────────────────────────────┘
Why one-way flow? Because it's predictable. When something goes wrong, you can trace the data from its source (the state) through the component tree to where it's displayed. Two-way binding (Angular 1, Backbone) makes this much harder:
TWO-WAY BINDING (hard to debug):
Model ←→ View ←→ Model ←→ View
"Where did this data change originate?" 🤷
ONE-WAY FLOW (easy to trace):
State → Component → User Action → State Update → Re-render
"The state changed in the click handler at line 42."
Philosophy 4: Explicit Over Implicit
React prefers explicit code over "magic":
// Angular (implicit — @Input decorator, ngOnChanges lifecycle)
@Component({
template: '<p>{{fullName}}</p>'
})
class UserComponent {
@Input() firstName: string;
@Input() lastName: string;
fullName: string;
ngOnChanges() {
this.fullName = `${this.firstName} ${this.lastName}`;
}
}
// React (explicit — props, direct calculation)
function User({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <p>{fullName}</p>;
}
In React, you can see exactly where data comes from, how it's transformed, and what gets rendered. There's no hidden lifecycle, no magic decorators, no implicit subscriptions.
8. How React Solves the DOM Problem
Let's go back to the notification system and see how React handles it:
The Vanilla JS Approach (What We Had)
// Store data in separate variables
let unreadCount = 0;
// Cache DOM references
const badge = document.getElementById('badge');
const mobileBadge = document.getElementById('mobile-badge');
const dropdown = document.getElementById('notification-list');
// Manually update EVERY UI element on each change
function onNotificationRead(id) {
unreadCount--;
// Update desktop badge
badge.textContent = unreadCount;
badge.style.display = unreadCount > 0 ? 'block' : 'none';
// Update mobile badge (easy to forget!)
mobileBadge.textContent = unreadCount;
mobileBadge.style.display = unreadCount > 0 ? 'block' : 'none';
// Update page title
document.title = unreadCount > 0 ? `(${unreadCount}) MyApp` : 'MyApp';
// Update the specific list item
const item = dropdown.querySelector(`[data-id="${id}"]`);
if (item) {
item.classList.remove('unread');
item.classList.add('read');
}
}
The React Approach
function NotificationCenter() {
const [notifications, setNotifications] = useState([]);
// Derived value — automatically correct
const unreadCount = notifications.filter(n => !n.read).length;
// Side effect — document title synced to unreadCount
useEffect(() => {
document.title = unreadCount > 0 ? `(${unreadCount}) MyApp` : 'MyApp';
}, [unreadCount]);
// Single action — everything else updates automatically
function markAsRead(id) {
setNotifications(prev =>
prev.map(n => n.id === id ? { ...n, read: true } : n)
);
}
return (
<>
{/* Desktop badge — automatically shows/hides */}
<DesktopBadge count={unreadCount} />
{/* Mobile badge — automatically shows/hides */}
<MobileBadge count={unreadCount} />
{/* Notification list — automatically updates */}
<ul className="notification-list">
{notifications.map(n => (
<li
key={n.id}
className={n.read ? 'read' : 'unread'}
onClick={() => markAsRead(n.id)}
>
{n.message}
</li>
))}
</ul>
</>
);
}
function DesktopBadge({ count }) {
if (count === 0) return null;
return <span className="badge desktop">{count}</span>;
}
function MobileBadge({ count }) {
if (count === 0) return null;
return <span className="badge mobile">{count}</span>;
}
What Changed — The Key Differences
| Aspect | Vanilla JS | React |
|---|---|---|
| Data location | Scattered variables | useState — single source of truth |
| Derived values | Must manually compute & update | Automatic — const unread = notifications.filter(...) |
| DOM updates | Manual, imperative, easy to forget | Automatic — React handles it |
| Adding new UI | Find every place count is used, add update | Just use unreadCount in new component |
| Bug risk | O(n) where n = number of DOM update sites | O(1) — only the state update logic |
| Testing | Need DOM environment | Pure function — call with props, check output |
The fundamental shift: you never think about "what DOM nodes need to change." You think about "what should the UI look like for this data?" React handles the rest.
9. React vs Vanilla JavaScript — A Real Comparison
Let's build a complete feature — a searchable, filterable product list — in both approaches.
Vanilla JavaScript Version
<!DOCTYPE html>
<html>
<body>
<h1>Product Catalog</h1>
<input id="search" placeholder="Search products..." />
<select id="category-filter">
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
</select>
<select id="sort">
<option value="name">Sort by Name</option>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
</select>
<p id="result-count"></p>
<div id="product-grid"></div>
<script>
const products = [
{ id: 1, name: 'Laptop', category: 'electronics', price: 999 },
{ id: 2, name: 'T-Shirt', category: 'clothing', price: 29 },
{ id: 3, name: 'JavaScript Book', category: 'books', price: 49 },
{ id: 4, name: 'Headphones', category: 'electronics', price: 199 },
{ id: 5, name: 'Jeans', category: 'clothing', price: 79 },
{ id: 6, name: 'React Book', category: 'books', price: 39 },
];
let searchTerm = '';
let categoryFilter = 'all';
let sortBy = 'name';
const searchInput = document.getElementById('search');
const categorySelect = document.getElementById('category-filter');
const sortSelect = document.getElementById('sort');
const resultCount = document.getElementById('result-count');
const grid = document.getElementById('product-grid');
function getFilteredProducts() {
let filtered = products;
if (searchTerm) {
filtered = filtered.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
if (categoryFilter !== 'all') {
filtered = filtered.filter(p => p.category === categoryFilter);
}
filtered.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price-asc') return a.price - b.price;
if (sortBy === 'price-desc') return b.price - a.price;
return 0;
});
return filtered;
}
function render() {
const filtered = getFilteredProducts();
resultCount.textContent = `${filtered.length} product${filtered.length !== 1 ? 's' : ''} found`;
grid.innerHTML = '';
filtered.forEach(product => {
const card = document.createElement('div');
card.className = 'product-card';
card.innerHTML = `
<h3>${product.name}</h3>
<p class="category">${product.category}</p>
<p class="price">$${product.price}</p>
<button data-id="${product.id}">Add to Cart</button>
`;
card.querySelector('button').addEventListener('click', () => {
alert(`Added ${product.name} to cart!`);
});
grid.appendChild(card);
});
}
searchInput.addEventListener('input', (e) => {
searchTerm = e.target.value;
render();
});
categorySelect.addEventListener('change', (e) => {
categoryFilter = e.target.value;
render();
});
sortSelect.addEventListener('change', (e) => {
sortBy = e.target.value;
render();
});
render();
</script>
</body>
</html>
React Version
import { useState, useMemo } from 'react';
const PRODUCTS = [
{ id: 1, name: 'Laptop', category: 'electronics', price: 999 },
{ id: 2, name: 'T-Shirt', category: 'clothing', price: 29 },
{ id: 3, name: 'JavaScript Book', category: 'books', price: 49 },
{ id: 4, name: 'Headphones', category: 'electronics', price: 199 },
{ id: 5, name: 'Jeans', category: 'clothing', price: 79 },
{ id: 6, name: 'React Book', category: 'books', price: 39 },
];
function ProductCatalog() {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
const filtered = useMemo(() => {
let result = PRODUCTS;
if (search) {
result = result.filter(p =>
p.name.toLowerCase().includes(search.toLowerCase())
);
}
if (category !== 'all') {
result = result.filter(p => p.category === category);
}
return [...result].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price-asc') return a.price - b.price;
return b.price - a.price;
});
}, [search, category, sortBy]);
return (
<div>
<h1>Product Catalog</h1>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search products..."
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
</select>
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
</select>
<p>{filtered.length} product{filtered.length !== 1 ? 's' : ''} found</p>
<div className="product-grid">
{filtered.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
function ProductCard({ product }) {
return (
<div className="product-card">
<h3>{product.name}</h3>
<p className="category">{product.category}</p>
<p className="price">${product.price}</p>
<button onClick={() => alert(`Added ${product.name} to cart!`)}>
Add to Cart
</button>
</div>
);
}
Side-by-Side Analysis
| Metric | Vanilla JS | React |
|---|---|---|
| Total lines | ~80 | ~65 |
| DOM queries | 5 (getElementById, etc.) | 0 |
| Event listener setup | 4 manual addEventListener | Inline, declarative |
innerHTML usage | Yes (XSS risk) | No — JSX escapes by default |
| Memory management | Manual (listeners on card buttons) | Automatic (React cleanup) |
| Reusability | None (everything is monolithic) | ProductCard is reusable |
| Adding a new filter | Add DOM element + listener + modify render() | Add state variable + JSX |
| Type safety | None | Full TypeScript support possible |
10. Why Not Just Use Templates?
Some frameworks use HTML templates with special syntax:
<!-- Angular template -->
<ul>
<li *ngFor="let todo of todos" [class.done]="todo.completed">
{{ todo.text }}
</li>
</ul>
<!-- Vue template -->
<ul>
<li v-for="todo in todos" :key="todo.id" :class="{ done: todo.completed }">
{{ todo.text }}
</li>
</ul>
React chose JSX instead:
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'done' : ''}>
{todo.text}
</li>
))}
</ul>
The Template vs JSX Trade-off
| Aspect | Templates (Angular/Vue) | JSX (React) |
|---|---|---|
| Iteration | *ngFor, v-for (custom syntax) | Array.map() (standard JS) |
| Conditionals | *ngIf, v-if (custom syntax) | &&, ? :, if/else (standard JS) |
| Data binding | {{ }}, [], () | { } (single syntax) |
| Learning curve | Must learn template DSL | Just know JavaScript |
| Type checking | Limited (Angular has good support) | Full TypeScript integration |
| Debugging | Template-specific error messages | Standard JS stack traces |
| Refactoring | Template strings are hard to refactor | Regular functions, IDE support |
| Power | Limited to what directives provide | Full JavaScript expressiveness |
Example: Complex Conditional Rendering
// React — just JavaScript logic
function UserDashboard({ user, subscription, notifications }) {
if (!user) return <LoginPrompt />;
const isTrialExpiring = subscription.type === 'trial'
&& subscription.daysRemaining < 7;
const criticalNotifications = notifications.filter(
n => n.priority === 'critical' && !n.dismissed
);
return (
<div>
<h1>Welcome, {user.name}</h1>
{isTrialExpiring && (
<TrialWarning daysLeft={subscription.daysRemaining} />
)}
{criticalNotifications.length > 0 && (
<AlertBanner notifications={criticalNotifications} />
)}
{user.role === 'admin' ? (
<AdminPanel />
) : (
<UserPanel subscription={subscription} />
)}
</div>
);
}
Try expressing this in a template language — it's possible, but the logic gets awkward because you're fighting the template's limitations.
11. React's Ecosystem and Popularity
By the Numbers (2026)
| Metric | Value |
|---|---|
| npm weekly downloads | ~25 million |
| GitHub stars | ~230,000+ |
| Stack Overflow questions | ~450,000+ |
| Companies using it | Meta, Netflix, Airbnb, Uber, Twitter/X, Discord, Notion, Shopify |
| Job postings | #1 frontend library globally |
The React Ecosystem Map
┌───────────────────────────────────────────────────┐
│ THE REACT ECOSYSTEM │
├───────────────────────────────────────────────────┤
│ │
│ CORE │
│ ├── React (UI rendering engine) │
│ └── ReactDOM (browser DOM binding) │
│ │
│ ROUTING │
│ ├── React Router (declarative client routing) │
│ └── TanStack Router (type-safe, file-based) │
│ │
│ STATE MANAGEMENT │
│ ├── useState/useReducer (local component state) │
│ ├── Context API (simple cross-component) │
│ ├── Zustand (lightweight global store) │
│ ├── Redux Toolkit (enterprise-grade store) │
│ ├── Jotai (atomic state model) │
│ └── Recoil (Facebook's atomic approach) │
│ │
│ SERVER STATE / DATA FETCHING │
│ ├── TanStack Query (caching, sync, mutations) │
│ ├── SWR (stale-while-revalidate) │
│ └── Apollo Client (GraphQL integration) │
│ │
│ FORMS │
│ ├── React Hook Form (performant, minimal) │
│ ├── Zod (schema validation) │
│ └── Formik (legacy, still used) │
│ │
│ STYLING │
│ ├── Tailwind CSS (utility-first CSS) │
│ ├── CSS Modules (scoped CSS) │
│ ├── styled-components (CSS-in-JS) │
│ └── Panda CSS (build-time CSS-in-JS) │
│ │
│ FULL-STACK FRAMEWORKS (built on React) │
│ ├── Next.js (SSR, SSG, API routes) │
│ ├── Remix (nested routing, loaders) │
│ └── Gatsby (static sites, GraphQL) │
│ │
│ UI COMPONENT LIBRARIES │
│ ├── shadcn/ui (copy-paste components) │
│ ├── Radix UI (headless, accessible) │
│ ├── MUI (Material Design) │
│ └── Ant Design (enterprise components) │
│ │
│ TESTING │
│ ├── React Testing Library (component testing) │
│ ├── Vitest / Jest (unit testing) │
│ └── Playwright (end-to-end testing) │
│ │
│ BUILD TOOLS │
│ ├── Vite (fast dev server + bundler) │
│ └── Turbopack (Next.js bundler, Rust) │
│ │
│ MOBILE │
│ └── React Native (iOS + Android native apps) │
│ │
└───────────────────────────────────────────────────┘
React vs Competitors (2026)
| Feature | React | Vue 3 | Angular 17+ | Svelte 5 | Solid |
|---|---|---|---|---|---|
| Type | Library | Framework | Framework | Compiler | Library |
| Rendering | Virtual DOM | Virtual DOM | Change detection + Signals | No VDOM (compiled) | Fine-grained reactivity |
| Bundle size | ~40KB | ~33KB | ~130KB+ | ~2KB (compiled) | ~7KB |
| Learning curve | Moderate | Low-Moderate | High | Low | Low-Moderate |
| Job market | #1 globally | Strong in Asia/EU | Enterprise/gov | Growing niche | Small but growing |
| TypeScript | Excellent (JSX) | Good (templates limited) | Built-in (decorators) | Good (Runes) | Excellent (JSX) |
| Mobile | React Native | NativeScript | Ionic | SvelteNative | Limited |
| Backed by | Meta | Community (Evan You) | Community (Rich Harris) | Community | |
| Server rendering | Via Next.js/Remix | Nuxt | Angular Universal | SvelteKit | SolidStart |
12. When React Is NOT the Right Choice
React is powerful but not universal. Choose the right tool for the job:
Skip React When:
1. Simple static websites (marketing pages, blogs)
- Use: Astro, Hugo, 11ty, plain HTML/CSS
- Why: React adds ~40KB+ of JavaScript for zero benefit on content-only pages
2. Minimal interactivity ("sprinkle of JS")
- Use: htmx, Alpine.js, vanilla JavaScript
- Why: Don't need a full component model for a dropdown menu and form validation
3. Performance-critical micro-widgets
- Use: Preact (3KB React-compatible), Solid, or vanilla JS
- Why: Every kilobyte matters when you're embedding in a third-party page
4. Team doesn't know JavaScript well
- Use: Angular (more guardrails), or invest in JS training first
- Why: React is "just JavaScript" — you need solid JS fundamentals
5. Server-rendered, low-JS applications
- Use: Ruby on Rails + Hotwire, Laravel + Livewire, Django + htmx
- Why: Full-stack server frameworks handle UI updates without client-side JS
Use React When:
- Building interactive web applications with complex state
- Multiple developers working on the same UI (components help collaboration)
- You need a large ecosystem with solutions for every problem
- You want code reuse between web and mobile (React Native)
- Building a long-lived product (React's stability and backward compatibility are excellent)
- Team knows JavaScript well (or will invest in learning)
- You're building a SPA (single-page application) or a complex dashboard
13. React's Evolution — From Classes to Hooks
React has gone through three major API eras. Understanding this helps you read legacy code and appreciate current best practices.
Era 1: createClass (2013–2016)
// The original React API — no ES6 classes
var Counter = React.createClass({
getInitialState: function() {
return { count: 0 };
},
handleClick: function() {
this.setState({ count: this.state.count + 1 });
},
render: function() {
return React.createElement('div', null,
React.createElement('p', null, 'Count: ' + this.state.count),
React.createElement('button', { onClick: this.handleClick }, '+1')
);
}
});
Era 2: Class Components with ES6 (2015–2019)
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this); // 😤 binding issues
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `Count: ${this.state.count}`;
}
}
componentWillUnmount() {
document.title = 'MyApp';
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>+1</button>
</div>
);
}
}
Problems:
thisbinding is confusing and error-prone- Related logic split across lifecycle methods (title update in
componentDidMountANDcomponentDidUpdate) - Sharing stateful logic between components required awkward patterns (HOCs, render props)
- Classes are hard for bundlers to optimize (tree-shaking, minification)
Era 3: Hooks — Functional Components (2019–Present)
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
return () => { document.title = 'MyApp'; }; // cleanup
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
What hooks fixed:
- No
thiskeyword at all - Related logic stays together (title logic in one
useEffect, not split across lifecycle methods) - Logic sharing via custom hooks (extract
useDocumentTitleand reuse everywhere) - Functions are simpler to understand than classes
- Better minification and optimization
Era 4: Server Components (2024+)
// This component runs on the SERVER — no JavaScript sent to the browser
async function ProductPage({ params }) {
const product = await db.products.findById(params.id);
const reviews = await db.reviews.findByProductId(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
<ReviewList reviews={reviews} />
{/* Only this component is a Client Component — only this ships JS */}
<AddToCartButton productId={product.id} />
</div>
);
}
Server Components are covered in depth in Topics 2.18–2.21.
14. The Mental Model Shift
The biggest challenge in learning React isn't the API — it's rewiring how you think about UI development.
Imperative Thinking (Pre-React)
You give the computer step-by-step instructions:
"When the user types in the search box..."
1. Get the search box element
2. Read its current value
3. Filter the product array
4. Clear the product grid
5. For each filtered product, create a card element
6. Add event listeners to each card's button
7. Append each card to the grid
8. Update the result count text
Declarative Thinking (React)
You describe what the end result should look like:
"The product grid shows filtered products based on the search term."
"The result count shows how many products are visible."
function ProductGrid({ products, searchTerm }) {
const filtered = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<p>{filtered.length} results</p>
{filtered.map(p => <ProductCard key={p.id} product={p} />)}
</div>
);
}
Analogies That Help
The Restaurant Analogy:
- Imperative: You go into the kitchen. Preheat oven. Chop onions. Saut in olive oil for 3 minutes...
- Declarative: "I'll have the pasta carbonara, please." The kitchen figures out how to make it.
- React is the kitchen. You describe what you want (JSX). React figures out the DOM operations.
The Spreadsheet Analogy:
Cell A1: 10
Cell A2: 20
Cell A3: =A1 + A2 → shows 30
Change A1 to 15 → A3 automatically shows 35. You never manually update A3.
React state = cells. React components = formulas. Change a state value → all dependent UI automatically updates.
The GPS Analogy:
- Imperative directions: "Turn left at the gas station. Go 2 miles. Turn right at the school..."
- Declarative destination: "Navigate to 123 Main Street." The GPS figures out the route.
- If you make a wrong turn with imperative directions, you're lost. If you make a wrong turn with GPS, it recalculates. React recalculates your UI whenever state changes — you can't get "lost."
15. Key Takeaways
-
React was born from real pain — Facebook's notification bug proved that manual DOM synchronization doesn't scale. React's solution: UI as a function of state.
-
The core problem: Keeping data (JavaScript) and UI (DOM) in sync manually is error-prone, hard to maintain, and impossible to scale.
-
jQuery helped but didn't solve the problem — It made DOM manipulation easier to write but not easier to manage at scale.
-
React is a library, not a framework — It handles UI rendering only. You choose everything else (routing, state management, styling).
-
UI = f(state)— Same state always produces the same UI. This makes React predictable, testable, and debuggable. -
Composition over inheritance — Build complex UIs by combining simple components, like LEGO blocks.
-
Unidirectional data flow — Data flows down (props), events flow up (callbacks). This makes data flow traceable.
-
JSX is "just JavaScript" — No special template syntax. Use
map(),&&, ternary operators — your JS skills transfer directly. -
React's ecosystem is massive — Whatever you need (routing, forms, state, styling, testing), there's a well-maintained library for it.
-
The mental shift is the hardest part — Move from "tell the DOM what to change" (imperative) to "describe what the UI should look like" (declarative). Once this clicks, everything else follows.
Explain-It Challenge
-
Explain to a non-programmer why building a complex website is harder than it looks. Use the counter example — start simple, then add requirements and watch the complexity explode. What does React do to tame that complexity?
-
Your friend says: "React just adds unnecessary complexity. I can build anything with vanilla JavaScript." How would you respond? In what cases would you actually agree with them?
-
Explain
UI = f(state)using a vending machine analogy. The vending machine's display (UI) is determined by what items are in stock (state). When stock changes, the display updates. How does this compare to manually changing the display every time someone buys something?
Navigation: ← Overview · Next → Declarative vs Imperative UI