- Explain components and JSX, and how JSX turns into function calls
- Pass data with props and keep changing data in
useState, updating it immutably - Load data with
useEffect(dependencies and cleanup) and render lists with stablekeys
Remember the to-do list from the DOM lesson: find the element, create an li, append it, toggle a class... As an app grows, keeping the page in sync with the data by hand turns into a mess. React, created at Facebook and open-sourced in 2013, flips the approach: you describe what the interface should look like for the current data, and React updates the DOM itself. In short: UI = f(state).
Components and JSX
A component is a function that returns a description of the interface; its name starts with a capital letter. JSX is HTML-like syntax inside JavaScript, and inside curly braces { } you can write any JavaScript expression. In JSX you use className instead of class, and camelCase names such as onClick for events. A component returns one root element (or a <>...</> fragment).
function Greeting({ name, city }) {
const hour = 9;
return (
<div className="card">
<h2>Hello, {name}!</h2>
<p>{city ? `You are in ${city}` : 'Where are you?'}</p>
<p>{hour < 12 ? 'Good morning' : 'Good afternoon'}</p>
</div>
);
}
export default function App() {
return (
<>
<Greeting name="Aysel" city="Baku" />
<Greeting name="Murad" />
</>
);
}JSX is just function calls: <h2 className="title">Hi</h2> becomes a call that creates an object describing the element's type, props and children. Let's repeat this ourselves with a tiny h function — what React calls the “virtual DOM” is exactly a tree of such objects.
function h(type, props, ...children) {
return { type, props: props ?? {}, children: children.flat() };
}
function render(node) {
if (typeof node === 'string' || typeof node === 'number') return String(node);
if (typeof node.type === 'function') return render(node.type(node.props));
const attrs = Object.entries(node.props)
.map(([key, value]) => ` ${key === 'className' ? 'class' : key}="${value}"`)
.join('');
return `<${node.type}${attrs}>${node.children.map(render).join('')}</${node.type}>`;
}
const Greeting = ({ name }) => h('h2', { className: 'title' }, 'Hello, ', name, '!');
const app = h('div', null, h(Greeting, { name: 'Aysel' }), h('p', null, 'Items: ', 3));
console.log(JSON.stringify(app.children[1]));
console.log(render(app));▸ Expected output
{"type":"p","props":{},"children":["Items: ",3]}
<div><h2 class="title">Hello, Aysel!</h2><p>Items: 3</p></div>Props
Props are a component's inputs, passed like HTML attributes: strings in quotes, everything else — numbers, arrays, objects, functions — in curly braces. Props are read-only: a component must not change them. Data flows down (parent → child), while events travel up through callbacks passed as props. The content between the tags goes into the children prop.
function ProductCard({ title, price, inStock = true, onBuy }) {
return (
<article className="product">
<h3>{title}</h3>
<p>{price.toFixed(2)} AZN</p>
{inStock ? (
<button onClick={() => onBuy(title)}>Buy</button>
) : (
<span>Out of stock</span>
)}
</article>
);
}
// inside a parent component:
<ProductCard title="Notebook" price={4.5} onBuy={(name) => console.log('Buying', name)} />
<ProductCard title="Pen" price={1} inStock={false} />Buying Notebook in the console. The second card says “Out of stock”.State with useState
State is data that changes over time and must update the interface: a counter, typed text, a list of tasks. const [value, setValue] = useState(initial) gives you the current value and a function that changes it. When setValue is called, React calls the component function again with the new value (a re-render). A direct assignment such as count = count + 1 does not update the screen. Hooks are called only at the top level of a component — never inside an if or a loop.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount((c) => c + 3)}>+3</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}let todos = [
{ id: 1, text: 'Learn JSX', done: true },
{ id: 2, text: 'Learn useState', done: false },
];
const addTodo = (list, text) => [...list, { id: list.length + 1, text, done: false }];
const toggleTodo = (list, id) =>
list.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo));
const removeTodo = (list, id) => list.filter((todo) => todo.id !== id);
const before = todos;
todos = addTodo(todos, 'Learn useEffect');
todos = toggleTodo(todos, 2);
todos = removeTodo(todos, 1);
console.log(todos.map((t) => `${t.done ? '[x]' : '[ ]'} ${t.text}`));
console.log(before === todos, before.length);▸ Expected output
[ '[x] Learn useState', '[ ] Learn useEffect' ] false 2
setTodos((list) => addTodo(list, text)). The old array stays untouched — React sees the new reference and updates the screen.Effects with useEffect
Rendering must be pure: it just computes the interface from props and state. Work that touches the outside world — loading data, timers, subscriptions — is a side effect and goes into useEffect(fn, dependencies). React runs the effect after the render has reached the screen. The dependency array controls the timing: [] — once after the first render; [userId] — every time userId changes; no array — after every render. The function returned from an effect is the cleanup: it cancels timers and requests when the component disappears or before the effect runs again.
import { useEffect, useState } from 'react';
function UserCard({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState('');
useEffect(() => {
const controller = new AbortController();
setUser(null);
fetch(`https://api.example.com/users/${userId}`, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setUser)
.catch((err) => {
if (err.name !== 'AbortError') setError(err.message);
});
return () => controller.abort();
}, [userId]);
if (error) return <p>Error: {error}</p>;
if (!user) return <p>Loading...</p>;
return <h2>{user.name}</h2>;
}userId changes, the cleanup cancels the old request — a late answer for the old user cannot overwrite the new one.Lists and keys
You render an array with map: JSX is returned for every item, and each one needs a key — an identifier that is unique among its siblings and stable (usually the id from the data). Keys let React recognise items between renders: when something is added, removed or moved, it knows which DOM node belongs to which item. If the list can change, don't use the index as the key.
function TodoList({ todos, onToggle }) {
if (todos.length === 0) return <p>Nothing to do!</p>;
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} className={todo.done ? 'done' : ''} onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
))}
</ul>
);
}- 1Create a project
Run in a terminal:
npm create vite@latest my-app -- --template react - 2Install the dependencies
cd my-appandnpm install - 3Start it
npm run dev, then openhttp://localhost:5173in a browser. The page updates instantly as you editsrc/App.jsx.
The useReducer hook works with reducers exactly like this. Complete cartReducer(cart, action): add adds a new product with qty: 1, increment increases a product's quantity by 1, remove deletes a product. Don't change any array or object — always return a new one.
function cartReducer(cart, action) {
switch (action.type) {
case 'add':
// return a new array with { id: action.id, qty: 1 } at the end
case 'increment':
// return a new array where that item has qty + 1
case 'remove':
// return a new array without that item
default:
return cart;
}
}
const start = [{ id: 'tea', qty: 1 }];
const actions = [
{ type: 'add', id: 'bread' },
{ type: 'increment', id: 'tea' },
{ type: 'increment', id: 'tea' },
{ type: 'remove', id: 'bread' },
];
console.log(actions.reduce(cartReducer, start));
console.log(start);▸ Expected output
[ { id: 'tea', qty: 3 } ]
[ { id: 'tea', qty: 1 } ]The h and render functions from the lesson are given. Use h to write a TodoList({ todos }) component: it returns a ul with one li per task, and the li of a finished task gets className: 'done'.
function h(type, props, ...children) {
return { type, props: props ?? {}, children: children.flat() };
}
function render(node) {
if (typeof node === 'string' || typeof node === 'number') return String(node);
if (typeof node.type === 'function') return render(node.type(node.props));
const attrs = Object.entries(node.props)
.map(([key, value]) => ` ${key === 'className' ? 'class' : key}="${value}"`)
.join('');
return `<${node.type}${attrs}>${node.children.map(render).join('')}</${node.type}>`;
}
// map todos to li elements inside the ul
const TodoList = ({ todos }) => h('ul', null);
const todos = [
{ id: 1, text: 'Learn JSX', done: true },
{ id: 2, text: 'Learn hooks', done: false },
];
console.log(render(h(TodoList, { todos })));▸ Expected output
<ul><li class="done">Learn JSX</li><li>Learn hooks</li></ul>
Key points
- A component is a function that returns UI from props; JSX compiles into function calls that describe elements.
- Props flow down and are read-only; events travel up through callbacks passed as props.
useStategives a value and a setter; calling the setter re-renders; update arrays and objects immutably.useEffectis for side effects after the render: the dependency array picks the timing, and the returned function cleans up.- Lists are rendered with
map, and each item needs a stable, uniquekey— not the index if the list can change.
Check yourself
10 questions. Every correct answer earns XP.