Skip to main content

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.

  1. Declarative UI: Instead of describing "how to manipulate the DOM," you describe "what the UI should look like given the current state."
  2. 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).

  1. Elements of different types produce different trees: If a div changes to a span, the entire subtree is replaced.
  2. 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

VersionKey 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.