Introduction to React — Declarative UI and Virtual DOM
What is React?
React is a JavaScript UI library that Facebook (now Meta) open-sourced in 2013. The fact that it is a "library" is important — unlike a "framework" such as Angular that includes routing, forms, and an HTTP client all in one, React focuses on the single responsibility of UI rendering.
React's core philosophy has two pillars.
- Declarative UI: Instead of describing "how to manipulate the DOM," you describe "what the UI should look like given the current state."
- Component-based: UI is broken down into independent, reusable pieces.
Imperative vs Declarative
// ❌ Imperative (Vanilla JS) — describes "how" to manipulate
const button = document.createElement('button');
button.textContent = 'Like';
button.classList.add('btn-primary');
button.addEventListener('click', () => {
count++;
button.textContent = `Like ${count}`;
});
document.body.appendChild(button);
// ✅ Declarative (React) — describes "what should be shown"
function LikeButton() {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Like {count}
</button>
);
}
Virtual DOM
Concept
The Virtual DOM is a lightweight JavaScript object copy of the real browser DOM. Whenever state changes, React creates a new Virtual DOM tree, compares it with the previous tree, and reflects only the changed parts in the actual DOM.
State change
↓
New Virtual DOM tree created
↓
Diffing against previous tree (Reconciliation algorithm)
↓
Minimal actual DOM updates
Reconciliation Algorithm
React's diffing algorithm makes two assumptions to reduce the O(n³) problem down to O(n).
- Elements of different types produce different trees: If a
divchanges to aspan, the entire subtree is replaced. - Child elements are identified by the key prop: Elements with the same key in a list are reused.
// Without key, the whole list re-renders
function BadList({ items }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li> // ❌ Index is a bad key
))}
</ul>
);
}
// Using unique keys
function GoodList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li> // ✅ Unique ID used
))}
</ul>
);
}
Fiber Architecture (React 16+)
React 16 introduced a new reconciliation engine called Fiber. Fiber breaks rendering work into small units, assigns priorities, and processes important updates (like user input) first.
- Concurrent Mode: Rendering can be paused, resumed, or abandoned.
- Time Slicing: Long renders are split across multiple frames to keep the UI responsive.
React Version History
| Version | Key Changes |
|---|---|
| React 16 (2017) | Fiber architecture, Error Boundary, Portal, Fragment |
| React 17 (2020) | Event delegation change, JSX Transform (no import needed) |
| React 18 (2022) | Concurrent by default, createRoot, useTransition, useDeferredValue, enhanced Suspense |
| React 19 (2024) | Server Components stable, Actions, use() hook, useActionState, useOptimistic, React Compiler |
React 18 Key Changes
createRoot API
// React 17 and below (legacy)
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// React 18+ (current)
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
Automatic Batching
Before React 18, state updates outside event handlers (such as in setTimeout or Promises) were not batched.
// Before React 18: 2 renders
setTimeout(() => {
setCount(c => c + 1); // render 1
setFlag(f => !f); // render 2
}, 1000);
// React 18: automatically batched into 1 render
setTimeout(() => {
setCount(c => c + 1); // batched
setFlag(f => !f); // batched → merged into 1 render
}, 1000);
React 19 Key Changes
Server Components
Components that render only on the server and are not included in the client bundle.
// app/page.jsx (Next.js App Router) — Server Component
async function UserProfile({ userId }) {
// Can directly query the DB on the server
const user = await db.user.findById(userId);
return <h1>{user.name}</h1>;
}
Actions
A new pattern for form handling — pass a function to the action prop.
function AddToCart() {
async function addItem(formData) {
'use server'; // Next.js Server Action
const id = formData.get('productId');
await db.cart.add(id);
}
return (
<form action={addItem}>
<input name="productId" value="123" />
<button type="submit">Add to Cart</button>
</form>
);
}
React Compiler (React Forget)
The React 19 compiler automatically insertsuseMemo, useCallback, and React.memo, so developers no longer need to optimize manually.
// Before compilation (written by developer)
function ExpensiveComponent({ items, filter }) {
const filtered = items.filter(item => item.type === filter);
return <List items={filtered} />;
}
// After compilation (transformed by React Compiler)
function ExpensiveComponent({ items, filter }) {
const filtered = useMemo(
() => items.filter(item => item.type === filter),
[items, filter]
);
return <List items={filtered} />;
}
React Ecosystem Overview
React (Core UI)
├── State Management
│ ├── Zustand (lightweight, simple)
│ ├── Jotai (atomic)
│ ├── Redux Toolkit (large-scale)
│ └── Recoil (Facebook)
├── Data Fetching
│ ├── TanStack Query (React Query)
│ └── SWR (Vercel)
├── Routing
│ └── React Router v6
├── Meta Frameworks
│ ├── Next.js (Vercel, SSR/SSG)
│ ├── Remix (nested routes)
│ └── Gatsby (SSG)
├── Styling
│ ├── Tailwind CSS
│ ├── CSS Modules
│ └── styled-components / Emotion
└── Testing
├── React Testing Library
├── Jest
└── Playwright (E2E)
Practical Example: Todo List App Preview
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Learn Next.js', done: false },
]);
const [input, setInput] = useState('');
function addTodo() {
if (!input.trim()) return;
setTodos([
...todos,
{ id: Date.now(), text: input, done: false },
]);
setInput('');
}
function toggleTodo(id) {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
}
return (
<div>
<h1>Todo List</h1>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()}
placeholder="Enter a task..."
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</li>
))}
</ul>
<p>Remaining tasks: {todos.filter(t => !t.done).length}</p>
</div>
);
}
export default TodoApp;
Pro Tips
1. Think of React as a "state machine"
UI = f(state) — the same state always produces the same UI. Thinking in terms of this pure function perspective reduces bugs.
2. Identify unnecessary re-renders
Enable the "Highlight updates when components render" option in React DevTools to visually see which components are re-rendering.
3. Always use StrictMode
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
StrictMode renders components twice in development to catch side-effect bugs early.
4. The React 19 compiler is not a silver bullet
While the compiler automates optimizations, it requires that components be pure functions. Rendering logic mixed with side effects cannot be optimized even by the compiler.