React is a JavaScript library for building user interfaces out of independent pieces called components. It was created at Meta (then Facebook) in 2013, and today Facebook, Instagram, WhatsApp Web, Netflix, Airbnb, Discord and thousands of dashboards, admin panels and online stores run on React. In this topic we will look at the problem React solves, compare the same code written in plain JavaScript and in React, and meet the key ideas that the whole course is built on. There is nothing to install yet — we will create a project in the next topic.
The problem: working with the DOM by hand
Let's start with a simple counter in vanilla JavaScript. The markup:
<p>Clicks: <span id="count">0</span></p>
<button id="inc">+1</button>
<button id="reset">Reset</button>And the script that brings it to life:
let count = 0
const countEl = document.querySelector('#count')
const incBtn = document.querySelector('#inc')
const resetBtn = document.querySelector('#reset')
incBtn.addEventListener('click', () => {
count++
countEl.textContent = count // don't forget to update the DOM!
})
resetBtn.addEventListener('click', () => {
count = 0
countEl.textContent = count // and here too
})The code works, but notice that the data (count) and the way it is displayed (countEl.textContent) live separately, and you have to keep them in sync by hand in every handler. If tomorrow the counter value also has to appear in the site header, you will have to find every place where count changes and add an update for the second element. Miss one — and the interface shows stale data.
Dynamic lists make it even harder. Here is a task list with adding and removing:
const tasks = ['Learn JSX', 'Build a counter']
const list = document.querySelector('#tasks')
const form = document.querySelector('#task-form')
const input = document.querySelector('#new-task')
function renderTasks() {
list.innerHTML = tasks
.map((task, i) => `<li>${task} <button data-index="${i}">×</button></li>`)
.join('')
}
form.addEventListener('submit', (e) => {
e.preventDefault()
tasks.push(input.value)
input.value = ''
renderTasks() // after every change — redraw by hand
})
list.addEventListener('click', (e) => {
if (e.target.matches('button')) {
tasks.splice(Number(e.target.dataset.index), 1)
renderTasks()
}
})
renderTasks()This approach has several problems at once:
- Everything is redrawn from scratch.
innerHTMLremoves and recreates every<li>: on long lists this is slow, and if there were input fields inside, they would lose focus and the typed text. - XSS vulnerability. If a user types
<img src=x onerror="alert('hacked')">, the string goes intoinnerHTMLas real HTML and the script runs. - Easy to forget an update. Every data change must end with a call to
renderTasks(). - Scattered logic. Markup in the HTML file, a template string in JS, handlers via delegation and
data-index— to understand how the interface works you have to keep all three places in your head.
The same code in React
Now let's look at the counter in React. We will cover the syntax (JSX, useState) in detail in the next topics — for now the idea itself is what matters.
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Clicks: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}There is not a single querySelector or textContent here. We described what the interface should look like for any value of count, and when a button is clicked we only change the data with setCount. React does the rest: it calls the Counter function again, compares the result with the previous one and updates only the text inside <p> in the DOM. If the value is also needed in the header, you just write {count} in the right place of the markup — the update happens automatically.
The task list in React:
import { useState } from 'react'
function TaskList() {
const [tasks, setTasks] = useState([
{ id: 1, title: 'Learn JSX' },
{ id: 2, title: 'Build a counter' },
])
const [text, setText] = useState('')
function handleSubmit(e) {
e.preventDefault()
setTasks([...tasks, { id: crypto.randomUUID(), title: text }])
setText('')
}
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button>Add</button>
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.title}{' '}
<button type="button" onClick={() => setTasks(tasks.filter((t) => t.id !== task.id))}>
×
</button>
</li>
))}
</ul>
</form>
)
}Markup, data and handlers are now in one place. When a task is added, React creates just one new <li> in the DOM and leaves the other elements alone. And the string <img src=x onerror=...> is shown as plain text: React escapes everything rendered through {}, so this class of XSS attacks is ruled out by default.
Declarative UI: UI = f(state)
React's main idea fits into the formula UI = f(state): the interface is a function of state. Instead of writing instructions like "find the element, hide the button, show the greeting", you describe what should be on the screen for every possible state. This style is called declarative, while manual DOM commands are imperative.
Imperative (vanilla JS):
function updateHeader(user) {
if (user) {
loginBtn.hidden = true
greeting.textContent = `Welcome back, ${user.name}!`
greeting.hidden = false
} else {
loginBtn.hidden = false
greeting.hidden = true
}
}Declarative (React):
function Header({ user }) {
if (!user) {
return <button>Log in</button>
}
return <p>Welcome back, {user.name}!</p>
}In the second version you cannot end up in a "broken" state where the login button and the greeting are visible at the same time: exactly one result is described for each value of user.
| Imperative approach | Declarative approach | |
|---|---|---|
| What you write | Steps: find, change, hide | The result for each state |
| Who updates the DOM | You, by hand | React |
| Where the truth about data lives | Spread between variables and the DOM | Only in state |
| Typical bugs | Forgotten updates, out-of-sync UI | Rare: the screen always follows from the data |
Components and one-way data flow
A component is a function that returns a piece of the interface. Bigger components are assembled from small ones, like building blocks: button → product card → catalog → page. The same component can be reused many times with different data.
import { useState } from 'react'
function ProductCard({ title, price, onAdd }) {
return (
<article className="card">
<h3>{title}</h3>
<p>{price} AZN</p>
<button onClick={onAdd}>Add to cart</button>
</article>
)
}
function Shop() {
const [cartCount, setCartCount] = useState(0)
return (
<main>
<header>Cart: {cartCount}</header>
<ProductCard title="Laptop" price={1899} onAdd={() => setCartCount(cartCount + 1)} />
<ProductCard title="Mouse" price={35} onAdd={() => setCartCount(cartCount + 1)} />
</main>
)
}Components form a tree:
Shop ← holds the cartCount state
├── header ← displays cartCount
├── ProductCard ← receives title, price, onAdd via props
└── ProductCardIn React, data flows one way — from top to bottom: the parent Shop passes data to the cards through props (component properties). A card cannot change cartCount itself — it only calls the onAdd function the parent gave it, and the parent changes its own state. Thanks to this one-way data flow it is always clear where data is stored and who changes it: to find the cause of a bug, you just walk up the tree.
Virtual DOM and reconciliation: the intuition
When you write JSX, React does not create DOM elements right away. First you get a plain JavaScript object — a description of what should be shown:
// JSX <p className="note">Clicks: {count}</p> with count = 0 becomes an object:
{
type: 'p',
props: { className: 'note', children: ['Clicks: ', 0] },
key: null,
}A tree of such objects is often called the virtual DOM. Creating objects is very cheap, while changing the real DOM is expensive: the browser has to recalculate styles and layout and repaint the page. So when state changes, React works like this:
- It calls the component function again and gets a new tree of objects.
- It compares it with the previous tree — this process is called reconciliation.
- It applies only the differences it found to the real DOM.
Render 1 (count = 0) Render 2 (count = 1)
<div> <div>
<p>Clicks: 0</p> <p>Clicks: 1</p> ← only the text changed
<button>+1</button> <button>+1</button>
</div> </div>A single text node changes in the DOM, while the buttons stay the same elements — with the same focus and the same handlers. We will study the render and commit phases in detail in the topic "How React Updates the Screen".
💡 The main value of the virtual DOM is not "speed at any cost" but a simple programming model: every time you describe the whole interface, and React takes care of the minimal DOM changes.
The React ecosystem in 2026
React is a library specifically for the interface. Routing, data fetching, forms and bundling are handled by separate tools, and over the years a huge ecosystem has grown around React. The current version is React 19 (19.3 at the time of writing): function components, hooks, Actions for forms, and the stable React Compiler, which automatically optimizes re-renders.
| Task | What people use in 2026 |
|---|---|
| SPA: user dashboard, admin panel | React + Vite (this course) |
| SEO-friendly site with server rendering, full-stack | Next.js, React Router in framework mode |
| Mobile apps for iOS/Android | React Native + Expo |
| Routing in an SPA | React Router, TanStack Router |
| Server data and caching | TanStack Query |
| Global state | Zustand, Redux Toolkit |
| Forms and validation | React Hook Form + Zod |
| Tests | Vitest + React Testing Library |
You will learn almost everything in this table during the course. Knowing React also opens the door to Next.js and to mobile development with React Native — the principles of components, props and state are the same there.
Common mistakes
1. Changing the DOM by hand inside a React component. React doesn't know about your changes and will overwrite them on the next render.
// ❌ Bad: bypassing React
function handleClick() {
document.querySelector('#count').textContent = count + 1
}
// ✅ Good: change the data, React updates the DOM itself
function handleClick() {
setCount(count + 1)
}2. Storing data in the DOM and reading it back. Code like Number(countEl.textContent) + 1 turns the DOM into the source of truth. In React the source of truth is state, and the DOM merely reflects it.
3. Expecting a regular variable to update the screen. If you declare let count = 0 and write count++ in a handler, the value changes, but React never finds out and does not re-render the component. Data that changes over time needs useState — we will study it in detail in the section on state.
4. Treating React as a framework where "everything is included". React has no built-in router, HTTP client or form validation. This is not a drawback but a deliberate choice: you plug in proven libraries that fit the task.
5. Starting React without solid JavaScript. Most of the "React problems" beginners have are actually gaps in JS. Before the course, make sure you are comfortable with arrow functions, destructuring, the spread operator, the map/filter array methods, import/export modules, template literals, the ternary operator and async/await.
Summary
- React is a library for building interfaces from components; routing, data and forms are handled by separate libraries from the ecosystem.
- Manual DOM work forces you to sync data and screen in every handler — hence out-of-sync UI, needless redraws and XSS via
innerHTML. - React is declarative: UI = f(state). You describe the result for each state, and React updates the DOM itself.
- Components are functions that return markup; the interface tree is built from them.
- Data flows down through props, and calls to handler functions travel back up.
- The virtual DOM and reconciliation let React change only what has actually changed in the real DOM.
- The current stack in 2026: React 19, Vite for SPAs, Next.js and React Router for full-stack, React Native for mobile apps.
Practice
- Add a second place that displays
countto the vanilla counter from the first section (for example, an<h1>with the text "Clicks: 0"). How many lines did you have to change? Describe in words what you would need to change in the React version. - Open any online store and draw the component tree of a product page in text form (header, gallery, price block, reviews, etc.). Mark which component should hold the "number of items in the cart" state.
- Imagine typing the string
<b>bold</b>into the vanilla task list. What will the page show? And the React version? Explain the difference. - Design a weather widget for the city of Baku: list the possible states (loading, error, data received), the components and their props. For each state, write in "pseudo-JSX" what should be on the screen.
💡 Task 4 doesn't need working code — the goal is to learn to think in states: "with this data, the screen looks like this".