- Объяснять, что такое компоненты и JSX и как JSX превращается в вызовы функций
- Передавать данные через props и хранить изменяющиеся данные в
useState, обновляя их без изменения исходных - Загружать данные через
useEffect(зависимости и очистка) и выводить списки со стабильнымиkey
Вспомни список дел из урока про DOM: найти элемент, создать li, добавить его, переключить класс... Когда приложение растёт, вручную держать страницу в согласии с данными становится хаосом. React, созданный в Facebook и открытый в 2013 году, переворачивает подход: ты описываешь, как должен выглядеть интерфейс для текущих данных, а DOM React обновляет сам. Коротко: UI = f(состояние).
Компоненты и JSX
Компонент — функция, которая возвращает описание интерфейса; его имя начинается с заглавной буквы. JSX — похожая на HTML запись внутри JavaScript, а в фигурных скобках { } можно писать любое выражение JavaScript. В JSX вместо class пишут className, а для событий — имена в camelCase, например onClick. Компонент возвращает один корневой элемент (или фрагмент <>...</>).
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 — это просто вызовы функций: <h2 className="title">Hi</h2> превращается в вызов, который создаёт объект с типом элемента, props и дочерними элементами. Повторим это сами с маленькой функцией h — то, что в React называют «виртуальным DOM», и есть дерево таких объектов.
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));▸ Ожидаемый результат
{"type":"p","props":{},"children":["Items: ",3]}
<div><h2 class="title">Hello, Aysel!</h2><p>Items: 3</p></div>Props
Props — входные данные компонента, их передают как атрибуты HTML: строки в кавычках, всё остальное — числа, массивы, объекты, функции — в фигурных скобках. Props только для чтения: компонент не должен их менять. Данные текут сверху вниз (родитель → ребёнок), а события поднимаются вверх через колбэки, переданные в props. Содержимое между тегами попадает в prop children.
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. На второй карточке написано «Out of stock».Состояние через useState
Состояние (state) — данные, которые меняются со временем и должны обновлять интерфейс: счётчик, введённый текст, список задач. const [value, setValue] = useState(начальное) даёт текущее значение и функцию для его изменения. При вызове setValue React снова вызывает функцию компонента с новым значением (повторный рендер). Прямое присваивание вроде count = count + 1 экран не обновляет. Хуки вызываются только на верхнем уровне компонента — не внутри if или цикла.
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);▸ Ожидаемый результат
[ '[x] Learn useState', '[ ] Learn useEffect' ] false 2
setTodos((list) => addTodo(list, text)). Старый массив остался нетронутым — React видит новую ссылку и обновляет экран.Эффекты через useEffect
Рендер должен быть чистым: он лишь вычисляет интерфейс из props и состояния. Работа, затрагивающая внешний мир, — загрузка данных, таймеры, подписки — это побочные эффекты, их пишут в useEffect(fn, зависимости). React запускает эффект после того, как рендер попал на экран. Массив зависимостей управляет моментом: [] — один раз после первого рендера; [userId] — при каждом изменении userId; без массива — после каждого рендера. Функция, возвращённая из эффекта, — очистка: она отменяет таймеры и запросы, когда компонент исчезает или перед повторным запуском эффекта.
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 меняется, очистка отменяет старый запрос — запоздавший ответ для прежнего пользователя не перезапишет нового.Списки и ключи
Массив выводят через map: для каждого элемента возвращается JSX, и каждому нужен key — идентификатор, уникальный среди соседей и стабильный (обычно id из данных). Благодаря ключам React узнаёт элементы между рендерами: когда что-то добавляется, удаляется или перемещается, он знает, какой узел DOM принадлежит какому элементу. Если список может меняться, не используй индекс как ключ.
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>
);
}- 1Создай проект
Выполни в терминале:
npm create vite@latest my-app -- --template react - 2Установи зависимости
cd my-appиnpm install - 3Запусти
npm run dev, затем открой в браузереhttp://localhost:5173. Страница сразу обновляется, когда ты меняешьsrc/App.jsx.
Хук useReducer работает именно с такими редьюсерами. Допиши cartReducer(cart, action): add добавляет новый товар с qty: 1, increment увеличивает количество товара на 1, remove удаляет товар. Не изменяй ни один массив или объект — всегда возвращай новый.
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);▸ Ожидаемый результат
[ { id: 'tea', qty: 3 } ]
[ { id: 'tea', qty: 1 } ]Даны функции h и render из урока. С помощью h напиши компонент TodoList({ todos }): он возвращает ul, в котором на каждую задачу приходится один li, а li выполненной задачи получает 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 })));▸ Ожидаемый результат
<ul><li class="done">Learn JSX</li><li>Learn hooks</li></ul>
Главное
- Компонент — функция, которая по props возвращает интерфейс; JSX компилируется в вызовы функций, описывающие элементы.
- Props текут сверху вниз и доступны только для чтения; события поднимаются вверх через колбэки в props.
useStateдаёт значение и функцию-сеттер; вызов сеттера запускает повторный рендер; массивы и объекты обновляй без мутаций.useEffect— для побочных эффектов после рендера: массив зависимостей выбирает момент, а возвращённая функция выполняет очистку.- Списки выводят через
map, и каждому элементу нужен стабильный уникальныйkey— не индекс, если список может меняться.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.