Вы уже умеете создавать компоненты и собирать из них страницу. Но страница, которая не меняется, — это просто красивая картинка. Сегодня мы научим компоненты принимать данные (props), выводить списки из массива, помнить состояние (useState) и реагировать на пользователя. Именно на этих четырёх вещах держится интерфейс любого сайта на React — от корзины интернет-магазина до чата.
Передавать данные в компонент через props, использовать children, деструктуризацию и значения по умолчанию
Выводить список из массива через .map() и объяснить, зачем нужен key и почему индекс в роли ключа ломает список
Хранить изменяемые данные в useState и обновлять их через setter, в том числе функциональным способом
Никогда не менять состояние «на месте»: добавлять, удалять и обновлять элементы массивов и объектов через spread, filter и map
Обрабатывать события (onClick, onChange, onSubmit) и делать управляемые поля ввода
Показывать и прятать части интерфейса: &&, тернарный оператор, ранний return
Как работать с этой страницей
Блоки кода с вкладками App.jsx и style.css — это мини-проекты React прямо в браузере: слева код, справа результат, внизу консоль. Меняйте код и нажимайте ▶ Запустить. Им нужен интернет — React подгружается из сети. В задачах под кодом есть строка // ---- проверка (не менять) ----: это маленький «робот-тестировщик», который после запуска сам нажимает кнопки и вводит текст, а потом пишет результат в консоль — по нему задача проверяется автоматически. В настоящем проекте такой блок не нужен, и трогать его не надо. Подсказки спрятаны под замочком 🔒: сначала пробуйте сами.
И ещё одна важная вещь. В реальном проекте на Vite React в консоли браузера ругается на ошибки, например «Each child in a list should have a unique key». Здесь, в учебной песочнице, работает «боевая» (production) сборка React, и этих предупреждений нет. Поэтому ошибки с ключами и состоянием мы будем ловить по результату, а не по красным строкам. Это даже полезнее: так вы учитесь видеть баг глазами.
01 Props: данные в компонент
Компонент — это функция, которая возвращает кусок интерфейса (на прошлом уроке вы создавали их в Vite). Одна и та же функция должна уметь рисовать разные карточки: про самбусу, про курутоб, про шакароб. Для этого ей нужны входные данные. В React они называются props — от слова «properties», свойства.
Аналогия из жизни
Меню в чайхоне. Рамка, шрифт и расположение строки — это компонент: они одинаковые для всех блюд. А «Самбуса — 5 сомони» и «Курутоб — 35 сомони» — это props: у каждого блюда свои. Типографии не нужно печатать новый бланк для каждого блюда — меняется только то, что вписано в строки.
Props передают как атрибуты в HTML. Внутри компонента они приходят одним объектом в первом аргументе функции:
Обратите внимание на запись значений: строки можно писать в кавычках (name="Самбуса"), а всё остальное — числа, булевы значения, массивы, объекты, функции — в фигурных скобках (price={5}). Фигурные скобки здесь означают: «внутри — обычное JavaScript-выражение».
Объект с данными, который родитель передаёт ребёнку. Родитель пишет их как атрибуты, ребёнок получает как аргумент функции.
Односторонний поток
Данные идут только сверху вниз: от родителя к ребёнку. Ребёнок не может переписать props своего родителя.
children
Особый prop: всё, что написано между открывающим и закрывающим тегами компонента.
Props нельзя менять
Props доступны компоненту только для чтения. Компонент должен вести себя как чистая функция: получил данные — нарисовал по ним интерфейс, но сами данные не трогает. Если компоненту нужно что-то изменять — это уже не props, а состояние (useState, раздел 04).
Частая ошибка
Попытка «подправить» props внутри компонента: props.price = props.price * 2. В режиме разработки React сразу выдаст ошибку, а в боевой сборке изменение может «тихо» сломать другие места. Нужно значение посчитать в новой переменной: const total = price * 2;
children — вложенное содержимое
Компоненты можно вкладывать друг в друга, как обычные HTML-теги. Всё, что вы написали между <Panel> и </Panel>, придёт в компонент в prop children. Вы сами решаете, где его вывести.
Panel с children · Ctrl + Enter
// children — всё, что написали МЕЖДУ открывающим и закрывающим тегомfunctionPanel({ title, children }) {
return (
<section className="card">
<h3 style={{ margin: "0 0 6px" }}>{title}</h3>
{children}
</section>
);
}
export defaultfunctionApp() {
return (
<div>
<Panel title="Сегодня в MakeFirstStep">
<p>JavaScript, 18:00</p>
<p>Практика с ИИ, 19:30</p>
</Panel>
<Panel title="Объявление">
<button>Записаться</button>
</Panel>
</div>
);
}
Подарочная коробка. Сама коробка (рамка, бантик, надпись) — это компонент Panel. Что лежит внутри — children. Одну и ту же коробку можно использовать для конфет, для книги, для платка. Коробке неважно, что внутри, — её дело красиво это упаковать.
Компонент-«обёртка» с children — один из самых частых приёмов: карточки, модальные окна, секции страницы, кнопки с иконками.
Props приходят первым аргументом функции. Записать их удобно сразу в деструктуризации: function DishCard({ name, price }).
Подсказка 2
Внутри JSX значения подставляются в фигурных скобках: {name} — {price} сомони.
ЗАДАЧА 1.2
Панель с содержимым
лёгкая
Компонент Panel рисует рамку с заголовком, но два абзаца, которые положили внутрь тегов, куда-то пропадают. Верните их на место: выведите содержимое под заголовком.
Панель с содержимым · Ctrl + Enter
// Панель-рамка: должна показывать заголовок И всё, что положили внутрь теговfunctionPanel({ title }) {
return (
<section className="card">
<h3>{title}</h3>
</section>
);
}
export defaultfunctionApp() {
return (
<Panel title="Сегодня в MakeFirstStep">
<p>JavaScript, 18:00</p>
<p>Практика с ИИ, 19:30</p>
</Panel>
);
}
// ---- проверка (не менять) ----setTimeout(() => {
const panel = document.querySelector(".card");
console.log(panel.querySelector("h3").textContent);
console.log(panel.querySelectorAll("p").length);
console.log(panel.lastElementChild.textContent);
}, 100);
Всё, что лежит между тегами компонента, приходит в prop с особым именем children.
Подсказка 2
Добавьте children в список props: { title, children } — и выведите {children} после заголовка.
ЗАДАЧА 1.3
Куда пропала цена?
средняя
Цена плова не показывается: на странице только слово «сомони». Это найди баг: исправьте одно слово в коде. Подумайте, чем отличаются имена, которые компонент ожидает, и имена, которые ему передали. React на такое не ругается, а просто кладёт undefined.
Сравните имя prop в объявлении Price({ value, ... }) и в вызове <Price amount=... />.
Подсказка 2
Имена должны совпадать буква в букву. Либо замените amount на value в вызове, либо наоборот — в объявлении.
02 Деструктуризация, значения по умолчанию и rest
Писать props.name, props.price, props.city в каждой строке утомительно. Вы уже знаете из урока про объекты, как это сокращается: деструктуризация. В React её используют почти везде — прямо в скобках параметров функции.
Если родитель не передал prop, внутри будет undefined. Чтобы подставить запасное значение, используйте знак = в деструктуризации — как в параметрах обычной функции (урок 5).
Значение по умолчанию срабатывает только для undefined. Если передали null, 0 или пустую строку "", будет использовано именно это значение. Для null в JavaScript есть оператор ?? — вы знакомы с ним по уроку про условия. Разберём это в задаче 2.3.
Rest и spread: «раздать остальные props»
Бывает, что компонент сам использует один-два prop, а все остальные должен передать вложенному тегу — например, <input>. Здесь работают знакомые три точки:
{ label, ...rest } в параметрах — собирает все остальные props в объект rest;
<input {...rest} /> в JSX — раздаёт этот объект как набор атрибутов.
Маршрутка в Душанбе. Водитель берёт плату за проезд (это label — то, что нужно ему), а вашу просьбу «остановите у Зелёного базара» и сумку (это rest) он не оставляет себе, а просто передаёт дальше. Так и компонент: берёт, что ему нужно, а остальное пересылает дальше без разбирательств.
Совет
Используйте {...rest} осознанно. Если раздавать «всё подряд», в тег может попасть prop, который он не понимает, и это сложно отлаживать. Для простых компонентов лучше явно перечислять нужные props.
Две из трёх кнопок пока пустые и без цвета, потому что родитель не передал им label и color. Задайте значения по умолчанию: label = "Нажми", color = "gray".
Кнопка с запасным текстом · Ctrl + Enter
// Задайте значения по умолчанию: label = "Нажми", color = "gray"functionButton({ label, color }) {
return <button style={{ background: color }}>{label}</button>;
}
export defaultfunctionApp() {
return (
<div>
<Button label="Купить" color="green" />
<Button />
<Button label="Отмена" />
</div>
);
}
// ---- проверка (не менять) ----setTimeout(() => {
document.querySelectorAll("button").forEach((b) => console.log(b.textContent, b.style.background));
}, 100);
Знак = пишется прямо в деструктуризации, рядом с именем: { label = "...", color = "..." }.
Подсказка 2
Запись выглядит так: function Button({ label = "Нажми", color = "gray" }).
ЗАДАЧА 2.2
Поле, которое всё передаёт дальше
средняя
Компонент Field рисует подпись и поле ввода, но placeholder, type и maxLength, которые ему передают, теряются: у обоих полей обычный тип и без ограничений. Соберите остальные props через ...rest и передайте их в <input>.
Поле, которое всё передаёт дальше · Ctrl + Enter
// Field должен передавать «лишние» props на <input>. Сейчас placeholder, type и maxLength теряются.functionField({ label }) {
return (
<label>
{label}: <input />
</label>
);
}
export defaultfunctionApp() {
return (
<div>
<Field label="Имя" placeholder="Мадина" maxLength={10} />
<br />
<Field label="Пароль" type="password" placeholder="••••" />
</div>
);
}
// ---- проверка (не менять) ----setTimeout(() => {
document.querySelectorAll("input").forEach((i) => console.log(i.type, i.placeholder, i.maxLength));
}, 100);
В параметрах: { label, ...rest }. Теперь в rest лежат все props, кроме label.
Подсказка 2
В JSX раздайте их полю: <input {...rest} />. Значение -1 в ответе — это обычное «ограничения нет» для maxLength.
ЗАДАЧА 2.3
Предскажите, что покажут метки
средняя
В коде шесть меток с разными значениями text. Не запуская код, запишите в тетрадь, что появится в квадратных скобках у каждой. Потом запустите и сверьтесь. Затем исправьте Label: пусть для null тоже показывается «—». Для 0 и пустой строки поведение менять не нужно.
Предскажите, что покажут метки · Ctrl + Enter
// 1) Предскажите, что покажет каждая из шести меток. 2) Запустите и сверьтесь.// 3) Исправьте Label так, чтобы и для null показывалось «—».functionLabel({ text = "—" }) {
return <span>[{text}]</span>;
}
export defaultfunctionApp() {
return (
<div>
<Label />
<Label text="Плов" />
<Label text={undefined} />
<Label text={null} />
<Label text={0} />
<Label text="" />
</div>
);
}
// ---- проверка (не менять) ----setTimeout(() => {
document.querySelectorAll("span").forEach((s) => console.log(s.textContent));
}, 100);
Значение по умолчанию в скобках не срабатывает на null. Уберите его оттуда и разберитесь с запасным значением внутри функции.
Подсказка 2
Оператор ?? подставляет правую часть, если слева null или undefined, а 0 и "" оставляет как есть: const shown = text ?? "—";
03 Списки: .map() и key
Данные на сайте почти всегда приходят списком: товары в каталоге, сообщения в чате, студенты в группе. Писать <li> для каждого элемента вручную нельзя — длина списка заранее неизвестна. В React список рисуют так: берут массив и превращают каждый его элемент в кусок JSX методом .map(). С map вы познакомились на уроке про массивы — он возвращает новый массив, и React умеет выводить массив из JSX-элементов.
Список уроков через map · Ctrl + Enter
// Список из массива: .map() превращает каждый элемент в <li>const lessons = ["HTML и CSS", "JavaScript", "React Lite"];
export defaultfunctionApp() {
return (
<ul>
{lessons.map((lesson) => (
<li key={lesson}>{lesson}</li>
))}
</ul>
);
}
map превращает каждую строку в элемент. Для "JavaScript" получается <li>JavaScript</li>.
Получился массив из трёх JSX-элементов. React выводит их на страницу один за другим.
Всё это записано в фигурных скобках{ ... } — внутри JSX это «вставка JavaScript».
Зачем нужен key
Каждому элементу списка нужно дать особый prop key — уникальный «паспорт» элемента. По ключу React узнаёт, какой именно элемент изменился, добавился или исчез, и перерисовывает только нужное. Без ключей он не может отличить «удалили второй элемент» от «изменили содержимое второго и третьего».
Аналогия из жизни
Гардероб в чайхоне или театре. Вы сдаёте куртку и получаете номерок. Гардеробщик не запоминает ваше лицо и не смотрит на цвет куртки, а ищет по номеру. Если номерки выдавать «по порядку в очереди» (первый, второй, третий) и первый человек уйдёт, все остальные внезапно получат чужие номерки. Хороший номерок принадлежит человеку, а не месту в очереди. Именно так работают ключи.
Ключ должен быть:
уникальным среди соседей (в одном списке), но не обязательно на всей странице;
стабильным: у одного и того же элемента при каждой перерисовке один и тот же ключ;
взятым из данных: id из базы, уникальный артикул или логин — а не случайное число.
Карточки студентов по id · Ctrl + Enter
// Массив объектов: у каждого есть уникальный id — он и станет keyconst students = [
{ id: 1, name: "Мадина", city: "Душанбе" },
{ id: 2, name: "Фарход", city: "Хуҷанд" },
{ id: 3, name: "Зарина", city: "Бохтар" },
];
functionStudentCard({ name, city }) {
return (
<div className="card">
<b>{name}</b> — {city}
</div>
);
}
export defaultfunctionApp() {
return (
<div>
{students.map((student) => (
<StudentCard key={student.id} name={student.name} city={student.city} />
))}
</div>
);
}
Ключ пишется на самом внешнем элементе, который вы возвращаете из map — здесь это <StudentCard key=...>. Внутри компонента StudentCard ключ не нужен, и сам компонент его «не увидит»: key — служебный атрибут, он не попадает в props.
Что брать в качестве key
Вариант
Годится?
Почему
key={item.id}
Да, лучший
Принадлежит самому элементу и не меняется при удалении и сортировке
key={item} для строк
Да, если строки не повторяются
Само значение уникально — например, список названий уроков
key={index}
Только для списка, который никогда не меняется
Индекс — это номер места, а не паспорт элемента. Подробности — в разделе 06
key={Math.random()}
Нет, никогда
Каждая перерисовка создаёт новые ключи, и React пересоздаёт все элементы с нуля
Частая ошибка
Вместо стрелочной функции с круглыми скобками (tag) => ( ... ) написать фигурные (tag) => { ... } и забыть слово return. Функция честно отработает и вернёт undefined — а вместо списка на странице будет пусто. Это то самое правило из урока про функции: у стрелочной функции с { } результат нужно возвращать явно. Именно в такую ловушку вы попадёте в задаче 3.4.
Совет
Фильтрация и вывод идут цепочкой, как на уроке про массивы: orders.filter(...).map(...). Сначала оставьте нужные элементы, потом превратите их в JSX. Если нужен счётчик («найдено: 2»), удобно сначала сохранить результат filter в переменную и взять у неё .length.
В массиве orders заказы чайханы. Покажите только неоплаченные — по одному <li> в формате «Клиент — сумма сомони». В абзаце над списком выведите, сколько их всего.
Посмотрите на скобки после стрелки: => {. Что в таких скобках нужно написать, чтобы функция вернула значение?
Подсказка 2
Добавьте return перед <span ...> либо замените { } на круглые скобки ( ).
04 useState: компонент, который помнит
Props приходят снаружи и не меняются. Но на странице есть данные, которые меняются от действий пользователя: сколько раз нажали «лайк», что написано в поле, открыто ли меню. Эти данные называются состоянием (state). Для него в React есть хукuseState.
Сначала посмотрим, почему обычной переменной недостаточно. Запустите пример и понажимайте кнопку:
Обычная переменная не работает · Ctrl + Enter
// Так НЕ работает: обычная переменная не заставляет React перерисовать экран.// Нажмите кнопку несколько раз и сравните экран с консолью.
export defaultfunctionApp() {
let count = 0;
functionhandleClick() {
count = count + 1;
console.log("count в памяти:", count);
}
return (
<div>
<p>На экране: {count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
В консоли число растёт, а на экране всегда ноль. Две причины:
React перерисовывает компонент, только когда его об этом «попросили». Простое изменение переменной ему не видно.
При каждой перерисовке функция компонента выполняется заново с нуля, и let count = 0 снова становится нулём. Обычные переменные внутри компонента не переживают перерисовку.
Нужно место, где значение живёт между перерисовками, и способ сообщить React «значение изменилось, перерисуй». Это и делает useState:
useState возвращает пару: текущее значение и функцию, которой его меняют
Начальное значение
Аргумент useState(0). Используется только при первом показе компонента. При следующих перерисовках React отдаёт уже сохранённое значение.
Текущее значение
Первый элемент пары — count. Это обычная переменная, которую можно читать в JSX.
Сеттер
Второй элемент — setCount. Вызов setCount(5) сохраняет новое значение и просит React перерисовать компонент.
Имена в паре выбираете вы, но принято писать [что, setЧто]: [liked, setLiked], [name, setName], [items, setItems]. Это деструктуризация массива — вы знаете её с урока 7.
Аналогия из жизни
Табло на футбольном матче. Судьи (setScore) говорят: «гол!», и табло обновляется. Если зритель на трибуне просто сам себе подумает «уже 3:2» — табло не изменится, и никто больше об этом не узнает. Менять значение напрямую — это «подумать про себя». Позвать сеттер — это «официально сообщить судье».
Своё состояние у каждого компонента
Каждый экземпляр компонента хранит собственное состояние. Если вы вывели Counter дважды, у вас два независимых счётчика.
Когда новое значение зависит от старого («прибавить 1»), безопасно передавать в сеттер функцию: setCount((c) => c + 1). React сам подставит в c самое свежее значение. Почему это важно? Внутри одного обработчика переменная count — это «снимок» значения на момент перерисовки. Три вызова setCount(count + 1) подряд превращаются в три раза «поставить 0 + 1» — и счётчик вырастет только на 1.
Снимок и функциональное обновление
// count сейчас 0setCount(count + 1); // поставить 0 + 1setCount(count + 1); // поставить 0 + 1 (count всё ещё 0 в этом «снимке»)setCount(count + 1); // поставить 0 + 1 → итог: 1// функциональная записьsetCount((c) => c + 1); // 0 → 1setCount((c) => c + 1); // 1 → 2setCount((c) => c + 1); // 2 → 3 → итог: 3
Частая ошибка
Ожидать, что сразу после setCount(5) переменная count станет равна 5. Она изменится только при следующей перерисовке. Если нужно «прямо сейчас» работать с новым значением, положите его в отдельную переменную: const next = count + 1; setCount(next); console.log(next);
Правила хуков
Хуки вызывают только на верхнем уровне компонента — не внутри if, циклов и вложенных функций — и только в компонентах (функциях с именем с заглавной буквы). Так React не теряет порядок хранимых значений.
Хранить только необходимое
Если значение можно вычислить из другого состояния — не заводите под него отдельное. В задаче 4.2 число лайков вычисляется из флага liked: два значения, которые легко рассинхронизировать, превращаются в одно.
Оживите счётчик: «+» прибавляет 1, «−» вычитает 1, «Сброс» обнуляет. Для этого добавьте useState и обработчики onClick. Не забудьте импорт в самом верху файла.
Счётчик посетителей · Ctrl + Enter
// Оживите счётчик: добавьте useState и обработчики для трёх кнопок.// Не забудьте строку импорта в самом верху: import { useState } from "react";
export defaultfunctionApp() {
return (
<div>
<p>Счёт: 0</p>
<button>−</button> <button>+</button> <button>Сброс</button>
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
const $ = (s) => document.querySelector(s);
const $$ = (s) => [...document.querySelectorAll(s)];
const btn = (t) => $$("button").find((b) => b.textContent.trim() === t);
asyncfunctionclick(el) {
el.click();
awaitwait();
}
(async () => {
awaitwait();
console.log($("p").textContent);
awaitclick(btn("+"));
awaitclick(btn("+"));
awaitclick(btn("+"));
awaitclick(btn("−"));
console.log($("p").textContent);
awaitclick(btn("Сброс"));
console.log($("p").textContent);
})();
Начните с const [count, setCount] = useState(0); и замените в абзаце 0 на {count}.
Подсказка 2
Обработчики: onClick={() => setCount(count + 1)}, onClick={() => setCount(count - 1)}, onClick={() => setCount(0)}. Стрелка нужна, чтобы функцию не вызвать сразу при отрисовке.
ЗАДАЧА 4.2
Кнопка «лайк»
средняя
Сделайте кнопку лайка: по первому клику сердечко закрашивается (♥) и число растёт на 1, по второму всё возвращается назад (♡, исходное число). В состоянии храните только флаг liked, а число лайков вычисляйте из него.
Кнопка «лайк» · Ctrl + Enter
// Кнопка «лайк»: по клику сердечко закрашивается и счётчик растёт на 1, по второму клику — всё возвращается.// Подсказка по дизайну: хранить в состоянии нужно только liked, а число лайков можно вычислить.
export defaultfunctionApp() {
const likes = 12;
return <button>♡ {likes}</button>;
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
asyncfunctionclick(el) {
el.click();
awaitwait();
}
(async () => {
const b = document.querySelector.bind(document);
awaitwait();
console.log(b("button").textContent);
awaitclick(b("button"));
console.log(b("button").textContent);
awaitclick(b("button"));
console.log(b("button").textContent);
})();
Три вызова видят один и тот же «снимок» count. Нужно, чтобы каждый следующий вызов отталкивался от результата предыдущего.
Подсказка 2
Передайте в сеттер функцию: setCount((c) => c + 1), три раза подряд.
ЗАДАЧА 4.4
Три независимых счётчика
средняя
Сначала предскажите: что покажут три счётчика после двух нажатий на первом и одного на третьем? Затем добавьте в Counter prop step (по умолчанию 1) — кнопка должна прибавлять step — и задайте третьему счётчику step={10}.
Три независимых счётчика · Ctrl + Enter
// 1) Предскажите: что покажут три счётчика, если нажать «+» два раза на первом и один раз на третьем?// 2) Добавьте Counter prop step (по умолчанию 1): кнопка прибавляет step.// 3) Третьему счётчику задайте step={10}.
import { useState } from "react";
functionCounter({ step = 1 }) {
const [n, setN] = useState(0);
return (
<div className="card">
<span>Счёт: {n}</span> <button onClick={() =>setN(n + 1)}>+</button>
</div>
);
}
export defaultfunctionApp() {
return (
<div>
<Counter />
<Counter />
<Counter />
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
asyncfunctionclick(el) {
el.click();
awaitwait();
}
(async () => {
const buttons = () => [...document.querySelectorAll("button")];
awaitwait();
awaitclick(buttons()[0]);
awaitclick(buttons()[0]);
awaitclick(buttons()[2]);
document.querySelectorAll("span").forEach((s) => console.log(s.textContent));
})();
Состояние у каждого Counter своё, поэтому клики по первому не влияют на третий. Новая часть здесь — prop step.
Подсказка 2
В обработчике замените n + 1 на n + step, а в App у третьего вызова напишите <Counter step={10} />.
05 Главное правило: не менять состояние «на месте»
Состояние-массив и состояние-объект — самое частое место для ошибок новичков. Правило одно: никогда не меняйте состояние напрямую — всегда передавайте в сеттер новую копию. React сравнивает старое и новое значение «по ссылке». Если вы изменили тот же самый массив и передали его обратно, для React ничего не изменилось — он пропускает перерисовку.
Аналогия из жизни
Электронная очередь в банке. Если оператор молча зачеркнёт строчку в старом талоне, табло в зале этого не заметит. Чтобы все увидели изменение, нужно выдать новый талон и повесить его на табло. Новая копия массива — это новый талон: он и сообщает React «смотри, тут что-то поменялось».
Запустите пример и нажмите кнопку. В консоли «Лук» есть, а на экране его нет:
push — мутация (так нельзя) · Ctrl + Enter
// Так НЕ работает: push меняет массив «на месте», а setItems получает ту же самую ссылку.
import { useState } from "react";
export defaultfunctionApp() {
const [items, setItems] = useState(["Хлеб", "Молоко"]);
functionaddItem() {
items.push("Лук");
setItems(items);
console.log("в массиве:", items.length);
}
return (
<div>
<ul>
{items.map((item, i) => <li key={i}>{item}</li>)}
</ul>
<button onClick={addItem}>Добавить «Лук»</button>
</div>
);
}
Обратите внимание на запись { ...x, done: true }: это копия объекта x, в которой поле done заменено. Остальные поля скопированы как есть. Эту «копию с заменой» вы видели на уроке 7 в разделе про spread. Тот же приём работает и для состояния-объекта:
Spread делает поверхностную копию. Если внутри объекта лежит вложенный объект или массив (user.address.city), его нужно скопировать тоже: { ...user, address: { ...user.address, city: "Хуҷанд" } }. Поэтому в состоянии стараются хранить данные «плоскими».
Совет
Если сомневаетесь, не мутирую ли я состояние, посмотрите на метод. Мутируют: push, pop, shift, unshift, splice, sort, reverse. Не мутируют (возвращают новое): map, filter, slice, concat, spread. Для сортировки сначала сделайте копию: [...items].sort(...).
Найди баг. По нажатию на «Добавить» в список покупок должен добавляться «Лук», но на экране ничего не меняется. Исправьте addItem. Нажимать кнопку нужно один раз — ключом служит само название.
Какой метод массива меняет его «на месте»? Он как раз используется в обработчике.
Подсказка 2
Замените две строки одной: setItems([...items, "Лук"]);
ЗАДАЧА 5.2
Профиль студента
средняя
Два задания. (1) «С днём рождения» должна увеличивать возраст на экране, сейчас она его «мутирует». (2) Допишите rename: имя меняется на «Зарина», возраст остаётся тем, что был.
Профиль студента · Ctrl + Enter
// 1) «С днём рождения» не меняет возраст на экране — исправьте.// 2) Допишите rename: имя становится «Зарина», возраст остаётся прежним.
import { useState } from "react";
export defaultfunctionApp() {
const [user, setUser] = useState({ name: "Мадина", age: 19 });
functionbirthday() {
user.age = user.age + 1;
setUser(user);
}
functionrename() {
// ваш код
}
return (
<div>
<p>{user.name}, {user.age}</p>
<button onClick={birthday}>С днём рождения</button> <button onClick={rename}>Сменить имя</button>
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
asyncfunctionclick(el) {
el.click();
awaitwait();
}
(async () => {
const p = () => document.querySelector("p").textContent;
const b = () => document.querySelectorAll("button");
awaitwait();
console.log(p());
awaitclick(b()[0]);
console.log(p());
awaitclick(b()[1]);
console.log(p());
})();
Список дел уже нарисован. Допишите две функции: removeTask(id) удаляет задачу, toggleTask(id) переключает у задачи флаг done. Ни splice, ни присваивания полям объекта в состоянии использовать нельзя. Эти две операции — основа домашнего задания.
○ Сделать зарядку | ○ Купить лепёшки | ○ Позвонить маме
✓ Сделать зарядку | ○ Позвонить маме
Подсказка 1
Удаление — это «оставить всех, кроме одного»: метод filter с условием t.id !== id.
Подсказка 2
Переключение — map: для нужной задачи вернуть копию { ...t, done: !t.done }, для остальных — саму t. Не забудьте передать результат в setTasks.
06 Почему индекс в роли key ломает список
Теперь, когда у нас есть состояние и удаление, можно увидеть, зачем нужны «паспорта» элементов. Многие пишут key={index} — так проще, и сначала всё выглядит нормально. Проверим на опыте.
В каждой строке есть поле «заметка». Запустите пример с индексом, впишите в поле у Мадины «привет», потом нажмите «Удалить» напротив Мадины:
Здесь заметка уходит вместе с Мадиной, а остальные не затронуты.
Что произошло на самом деле
Было (key = index)
Стало после удаления Мадины
Что решил React
key=0 → Мадина + «привет»
key=0 → Фарход
«Строка 0 осталась. Поменялось только имя» — поле с текстом сохраняется
key=1 → Фарход
key=1 → Зарина
«Строка 1 осталась. Поменялось имя»
key=2 → Зарина
— (нет)
«Строка 2 исчезла» — удаляется последняя строка
С индексом React «удалил» последнюю строку и сдвинул данные вверх. Введённый текст остался в строке 0 — теперь она принадлежит Фарходу
С ключом id React видит: «ключ 1 пропал» — и убирает ровно ту строку (вместе с её полем ввода), а остальные оставляет как были.
То, что мы видим здесь на поле ввода, происходит и с другим «внутренним» состоянием элементов: фокусом курсора, выбранным чекбоксом, состоянием вложенных компонентов (useState внутри карточки), анимацией. Индекс подходит только для списков, которые не удаляются, не переставляются и не сортируются — и в которых нет полей ввода.
Частая ошибка
Генерировать ключ «на лету» — key={Math.random()} или key={crypto.randomUUID()} внутри map. Тогда при каждой перерисовке все ключи новые, React считает, что все элементы заменены, и пересоздаёт их с нуля: поля теряют фокус и введённый текст. Идентификатор нужно создавать один раз — при создании данных (например, когда пользователь добавляет задачу), и хранить его вместе с данными.
Практика: закрепляем
ЗАДАЧА 6.1
Заметки уехали к соседу
средняя
Найди баг. У каждого человека в списке есть поле для заметки. Проверка пишет заметки всем троим и удаляет Мадину. После этого у Фархода должна остаться его заметка «про Фарход», а у Зарины — «про Зарину». Сейчас они перепутаны. Исправьте одну строку.
Посмотрите на key у <li>. Что именно React использует, чтобы понять, какая строка пропала?
Подсказка 2
Замените key={index} на key={p.id}. Переменная index в скобках map после этого не нужна.
07 События и управляемые поля ввода
Состояние нужно, чтобы на что-то реагировать. А реагировать мы будем на события: клики, ввод текста, отправку формы. В React они подключаются как props с именем on… в стиле camelCase: onClick, onChange, onSubmit. В отличие от HTML (onclick="..."), значением служит не строка, а сама функция.
Самая частая ошибка с событиями: написать onClick={sayHi()} со скобками. Скобки вызывают функцию прямо во время отрисовки, и в onClick попадает не функция, а то, что она вернула. Запустите пример: «Салом!» появляется в консоли без единого клика.
sayHi() и sayHi · Ctrl + Enter
// Запустите и посмотрите в консоль: «Салом!» уже там, хотя мы ни разу не нажали кнопку.functionsayHi() {
console.log("Салом!");
}
export defaultfunctionApp() {
return (
<div>
<button onClick={sayHi()}>Неправильно: sayHi()</button>{" "}
<button onClick={sayHi}>Правильно: sayHi</button>
</div>
);
}
<button onClick={sayHi}>Без параметров</button>
<button onClick={() =>remove(item.id)}>С параметром: оборачиваем в стрелку</button>
<button onClick={() =>setCount(count + 1)}>Прямо в стрелке</button>
Если обработчику нужно передать аргумент (item.id), оборачивайте вызов в стрелочную функцию. Тогда вызов произойдёт не сейчас, а когда пользователь нажмёт.
Управляемое поле (controlled input)
В обычном HTML значением поля владеет браузер. В React принято иначе: значение живёт в состоянии, а поле только показывает его и сообщает об изменениях. Такое поле называется управляемым. Для этого нужны два атрибута:
value={name} — что показывать;
onChange={(e) => setName(e.target.value)} — как обновлять состояние, когда пользователь печатает. Новый текст лежит в e.target.value (у чекбокса — e.target.checked).
Управляемые поля: input, checkbox, select · Ctrl + Enter
Диктор и телесуфлёр. Вы печатаете слово, оно уходит в «центр» (состояние), и уже оттуда возвращается на экран в поле. Поле — это просто окошко, в котором показано значение из состояния. Пока состояние не обновилось, ничего нового в окошке не появится. Поэтому если написать value без onChange, поле «замёрзнет»: напечатать в нём ничего нельзя.
Формы: onSubmit и preventDefault
Форма по умолчанию при отправке перезагружает страницу. Состояние при этом сбрасывается — это ровно то, чего мы не хотим. Поэтому в начале обработчика onSubmit вызывают event.preventDefault() — знакомый вам приём из урока про формы.
Повесить onClick на кнопку формы вместо onSubmit на саму форму. Тогда Enter в поле не отправит форму. Правило: действие «отправить» — на <form onSubmit>, а кнопка внутри формы остаётся обычной.
Совет
Если в состоянии лежит текст из поля, не забывайте про .trim(): пользователь легко может отправить строку из одних пробелов. Проверка if (text.trim() === "") return; — привычка, которая спасёт от пустых задач в списке.
Сделайте поле поста с лимитом 20 символов: в <small> показывайте «длина/20», а кнопку «Опубликовать» отключайте атрибутом disabled, если текст пустой или длиннее лимита.
Пост с лимитом символов · Ctrl + Enter
// Поле для поста с лимитом 20 символов.// 1) Храните текст в состоянии (управляемое поле).// 2) В <small> показывайте «длина/20».// 3) Кнопка «Опубликовать» выключена (disabled), если текст пустой или длиннее 20 символов.
export defaultfunctionApp() {
return (
<div>
<textarea placeholder="Что нового?" />
<p><small>0/20</small></p>
<button>Опубликовать</button>
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
functiontypeInto(el, value) {
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value").set.call(el, value);
el.dispatchEvent(newEvent("input", { bubbles: true }));
}
const state = () => document.querySelector("small").textContent + " " + document.querySelector("button").disabled;
(async () => {
awaitwait();
console.log(state());
typeInto(document.querySelector("textarea"), "Салом");
awaitwait();
console.log(state());
typeInto(document.querySelector("textarea"), "я".repeat(25));
awaitwait();
console.log(state());
})();
Длину не нужно хранить отдельно: она всегда равна text.length. В состоянии — только text.
Подсказка 2
<button disabled={text.length === 0 || text.length > 20}> — атрибут принимает булево значение.
ЗАДАЧА 7.3
Форма добавления покупки
средняя
Допишите handleSubmit: отмените перезагрузку страницы, не добавляйте пустые строки и строки из одних пробелов, добавьте в список объект { id: Date.now(), text } и очистите поле. Идентификатор создаётся один раз — при добавлении, это и будет ключ.
Порядок действий: event.preventDefault() → const clean = text.trim() → если пусто, return → добавить → очистить поле через setText("").
Подсказка 2
Добавление в конец без мутации: setItems([...items, { id: Date.now(), text: clean }]);
ЗАДАЧА 7.4
Живой поиск городов
сложная
Список городов нужно фильтровать на лету по тексту из поля, без учёта регистра. Если ничего не найдено — показать <p>Ничего не найдено</p>. Состояние только одно — query; отфильтрованный список вычисляйте при каждой отрисовке.
Живой поиск городов · Ctrl + Enter
// Живой поиск: показывайте только города, в названии которых есть введённый текст (без учёта регистра).// Если ничего не найдено — выведите <p>Ничего не найдено</p>.
import { useState } from "react";
const cities = ["Душанбе", "Худжанд", "Бохтар", "Куляб", "Истаравшан"];
export defaultfunctionApp() {
const [query, setQuery] = useState("");
return (
<div>
<input value={query} onChange={(e) =>setQuery(e.target.value)} placeholder="Поиск города" />
<ul>
{cities.map((city) => <li key={city}>{city}</li>)}
</ul>
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
functiontypeInto(el, value) {
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value").set.call(el, value);
el.dispatchEvent(newEvent("input", { bubbles: true }));
}
(async () => {
awaitwait();
for (const q of ["ду", "УД", "xyz"]) {
typeInto(document.querySelector("input"), q);
awaitwait();
const names = [...document.querySelectorAll("li")].map((li) => li.textContent).join(", ");
console.log(q + ": " + (names || document.querySelector("p").textContent));
}
})();
Перед return вычислите const found = cities.filter(...) и выводите found вместо cities.
Подсказка 2
Условие: city.toLowerCase().includes(query.trim().toLowerCase()). Сообщение: {found.length === 0 && <p>Ничего не найдено</p>}.
08 Условный рендер: показать или спрятать
Интерфейс постоянно «ветвится»: у гостя одна кнопка, у вошедшего другая; пока данные грузятся — крутилка, потом список; корзина пустая — подсказка, не пустая — товары. В React для этого нет специальных тегов вроде v-if. Вы используете обычный JavaScript — условия и операторы, которые уже знаете. Есть три приёма.
1. Оператор &&: «показать или ничего»
Если нужно показать кусок интерфейса или не показывать совсем, пишут условие && <JSX />. Если условие ложно, получается false, а React ничего не рисует на месте false, null и undefined.
Если компонент в особом случае выглядит совсем иначе (загрузка, пусто, ошибка), удобно обработать его в начале функции и сразу сделать return. Основной вариант остаётся ниже, без вложенных условий.
Вывеска на магазине: «Открыто» или «Закрыто». Это тернарный оператор: всегда висит одна из двух табличек. А вот табличка «Скидка!» висит только если скидка есть — это &&. Если скидки нет, нет и таблички, а не табличка «Скидки нет».
Ловушка: «0 && …» рисует ноль
Оператор && возвращает левую часть, если она ложная. false, null, undefined React не показывает, но число 0 показывает как 0. Поэтому {items.length && <List />} при пустом списке выведет на странице цифру 0. Сравнивайте явно: {items.length > 0 && ...}.
Если условий становится много, не нагромождайте вложенные тернарные операторы — читать их невозможно. Вынесите логику в переменную перед return или в отдельный компонент с ранним return.
В компонент Dish нужно добавить бейдж <span>Новинка</span> рядом с названием, но только у тех блюд, у которых isNew истинно. Запись <Dish name="Манту" isNew /> без значения означает isNew={true}.
Пока пользователь не вошёл, user равен null, и Profile падает с ошибкой на user.name. Добавьте в начало Profile ранний return с текстом «Войдите в аккаунт».
Профиль без пользователя · Ctrl + Enter
// Profile падает, пока user === null. Добавьте ранний return:// если пользователя нет — вернуть <p>Войдите в аккаунт</p>.
import { useState } from "react";
functionProfile({ user }) {
return <p>Привет, {user.name}!</p>;
}
export defaultfunctionApp() {
const [user, setUser] = useState(null);
return (
<div>
<Profile user={user} />
<button onClick={() =>setUser(user ? null : { name: "Мадина" })}>{user ? "Выйти" : "Войти"}</button>
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
(async () => {
const show = () => console.log(document.querySelector("p").textContent);
awaitwait();
show();
document.querySelector("button").click();
awaitwait();
show();
document.querySelector("button").click();
awaitwait();
show();
})();
Войдите в аккаунт
Привет, Мадина!
Войдите в аккаунт
Подсказка 1
Условие для раннего выхода — «пользователя нет»: if (!user).
Подсказка 2
if (!user) return <p>Войдите в аккаунт</p>; — строкой выше основного return.
ЗАДАЧА 8.4
FAQ-аккордеон
сложная
Сделайте раскрывающиеся вопросы: клик по вопросу показывает ответ (<p className="answer">) и закрывает остальные, повторный клик по тому же вопросу закрывает ответ. Подумайте: что достаточно хранить в состоянии, чтобы одновременно мог быть открыт не больше чем один вопрос?
FAQ-аккордеон · Ctrl + Enter
// FAQ-аккордеон. Клик по вопросу открывает его ответ и закрывает остальные; повторный клик закрывает.// Ответ — <p className="answer">. Подумайте: что хранить в состоянии, чтобы был открыт не больше одного?
import { useState } from "react";
const faq = [
{ id: 1, q: "Сколько стоит курс?", a: "Ответ про цену" },
{ id: 2, q: "Нужен ли свой ноутбук?", a: "Ответ про ноутбук" },
{ id: 3, q: "Будет ли сертификат?", a: "Ответ про сертификат" },
];
export defaultfunctionApp() {
return (
<div>
{faq.map((item) => (
<div key={item.id} className="card">
<button>{item.q}</button>
{/* здесь должен быть ответ */}
</div>
))}
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
asyncfunctionclick(el) {
el.click();
awaitwait();
}
(async () => {
const shown = () => [...document.querySelectorAll(".answer")].map((a) => a.textContent).join(", ") ||"закрыто";
const q = (i) => document.querySelectorAll(".card button")[i];
awaitwait();
console.log(shown());
awaitclick(q(0));
console.log(shown());
awaitclick(q(1));
console.log(shown());
awaitclick(q(1));
console.log(shown());
})();
Достаточно одного значения — id открытого вопроса (или null, если все закрыты): const [openId, setOpenId] = useState(null);
Подсказка 2
По клику: setOpenId(openId === item.id ? null : item.id). Ответ показывайте так: {openId === item.id && <p className="answer">{item.a}</p>}.
09 ИИ-блок: ИИ объясняет ошибки React — а мы проверяем
Две ошибки React новички видят чаще всего: «я нажимаю, а экран не меняется» и «список ведёт себя странно после удаления». ИИ объясняет их быстро и убедительно. Но убедительно — не значит верно. Ваша задача сегодня — взять объяснение ИИ и проверить его на собственном коде: запустить, поломать, починить и убедиться, что причина названа правильно.
Правила курса, которые здесь особенно важны
1. Сначала 15 минут пробуете сами: вспомните разделы 04–06 этого урока. 2. Только потом идёте к ИИ. 3. Не вставляйте в проект код, который не можете объяснить строчка за строчкой — для React это значит: вы знаете, почему здесь новый массив, а не push, и откуда берётся каждый key. 4. Проверяйте ответ ИИ запуском: объяснение, которое нельзя проверить на коде, ничего не стоит.
Как просить: плохо и хорошо
Формула промпта курса: контекст → задача → ограничения → формат ответа → проверка. Два готовых промпта из библиотеки курса — для «состояние не обновляется» и «зачем нужен key» — уже построены по ней.
у меня в реакте не работает кнопка, почини
Я учу React на уровне новичка: знаю props, useState и .map(). Вот мой компонент: [вставьте код]. При нажатии на «Добавить» новый элемент не появляется на экране, хотя в консоли он есть в массиве. Объясни, почему useState не сработал и как исправить. Ограничения: без сторонних библиотек, без useEffect, поменяй как можно меньше строк. Формат ответа: 1) причина в двух предложениях простыми словами; 2) исправленный код целиком; 3) что именно изменилось. Проверка: в конце скажи, не мутирую ли я состояние напрямую.
зачем в реакте key
Объясни на моём примере, зачем React нужен key в .map() и что сломается без него: [вставьте код списка]. Я новичок. Формат: короткое объяснение с аналогией, затем покажи, как ломается список, если использовать index в качестве key, и как это исправить. Проверка: предложи, как мне самому убедиться в баге на удалении элемента из середины списка (какие поля ввода и какие действия нужны).
Чек-лист: как проверить объяснение ИИ
1) Состояние не обновляется: нашёл ли ИИ мутацию (push, obj.x = …, sort без копии)? Если он предложил «просто добавить useEffect» или «принудительно перерисовать» — это почти всегда лишнее. 2) Ключи: какой key он выбрал? Если index «чтобы убрать предупреждение» — это тот самый баг из раздела 06. Ключ должен приходить из данных. 3) Проверка запуском: воспроизведите баг на удалении из середины — с полем ввода в каждой строке — до и после исправления. 4) Можете ли вы своими словами объяснить, почему исправление работает?
Хороший ИИ-помощник объясняет, а не только чинит. Если в ответе вы не поняли хоть одно слово, спросите: «объясни проще, на примере чайханы». И не стесняйтесь спорить: если ИИ предложил key={index}, возразите ему примером из раздела 06 и посмотрите, как он выкрутится.
Практика: проверяем ИИ
ЗАДАЧА 9.1
Список дел «от ИИ»
сложная
Этот код выдал ИИ по запросу «сделай список дел на React». Он запускается, но в нём три ошибки: кнопка «Добавить» не обновляет экран, после удаления из середины заметки уезжают к соседям, а после «удалить → добавить» удаление последней строки сносит сразу две. Найдите и исправьте все три. Последняя ошибка коварная: задумайтесь, откуда берётся id новой задачи и может ли он повториться.
Список дел «от ИИ» · Ctrl + Enter
// Этот список дел «написал ИИ». Он запускается, но работает неправильно. Найдите ВСЕ ошибки и исправьте.// Признаки: после добавления список не обновляется; после удаления заметки уезжают к соседям;// после «удалить, потом добавить» удаление сносит сразу две строки.
import { useState } from "react";
export defaultfunctionApp() {
const [todos, setTodos] = useState([
{ id: 1, text: "Позвонить маме" },
{ id: 2, text: "Купить лепёшки" },
]);
functionaddTodo() {
todos.push({ id: todos.length + 1, text: "Новая задача" });
setTodos(todos);
}
return (
<div>
<ul>
{todos.map((t, i) => (
<li key={i}>
<b>{t.text}</b> <input placeholder="заметка" />{" "}
<button className="del" onClick={() =>setTodos(todos.filter((x) => x.id !== t.id))}>✕</button>
</li>
))}
</ul>
<button className="add" onClick={addTodo}>Добавить</button>
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
asyncfunctionclick(el) {
el.click();
awaitwait();
}
(async () => {
const $ = (s) => document.querySelector(s);
const $$ = (s) => [...document.querySelectorAll(s)];
const count = () => console.log($$("li").length);
awaitwait();
awaitclick($(".add"));
count();
$$("input").forEach((input, i) => (input.value = ["a", "b", "c"][i]));
awaitclick($$(".del")[0]);
console.log($$("li").map((li) => li.querySelector("b").textContent + ": " + li.querySelector("input").value).join(" | "));
awaitclick($(".add"));
count();
awaitclick($$(".del")[2]);
count();
})();
Три места: push + setTodos(todos) (мутация), key={i} (индекс вместо паспорта) и id: todos.length + 1 (после удаления длина уменьшается, и новый id совпадает с существующим).
Подсказка 2
Добавление: setTodos([...todos, { id: Date.now(), text: "Новая задача" }]). Ключ: key={t.id}. Идентификатор создаётся один раз — в момент добавления.
ЗАДАЧА 9.2
Свой промпт про key
лёгкая
Возьмите свой код из задачи 6.1 или 9.1 и составьте для ИИ промпт по формуле курса. Отправьте, получите ответ и прогоните его по чек-листу выше. Запишите в комментарии: какой key предложил ИИ, пришлось ли вам с ним спорить, и как вы убедились, что объяснение верное. Автоматической проверки нет — отметьте задачу вручную, когда будет готово.
Можно редактировать · Ctrl + Enter
// Мой промпт:// ...// Какой key предложил ИИ и что я проверил:// ...
Подсказка 1
Если ответ ИИ вас не убедил, не просите «исправь ещё раз», а назовите конкретный провал: «после удаления первой строки заметка осталась у второй».
Подсказка 2
Сверьте ответ с разделом 06 этого урока. Если ИИ утверждает что-то, чего там нет, — перепроверьте на коде.
10 Практика на уроке: каталог курсов с лайками
Соберём всё вместе: каталог курсов MakeFirstStep. Карточки рисуются из массива через props, у каждой есть кнопка «лайк», которая запоминает своё состояние. Идём шагами — после каждого шага код работает. Каждый шаг можно запускать и менять прямо здесь.
Шаг 1. Компонент и props
Сначала один компонент CourseCard и три вызова с разными данными.
Шаг 1. CourseCard и props · Ctrl + Enter
// Шаг 1. Компонент CourseCard и три карточки с разными propsfunctionCourseCard({ title, price }) {
return (
<div className="card">
<b>{title}</b>
<div>{price} сомони в месяц</div>
</div>
);
}
export defaultfunctionApp() {
return (
<div>
<CourseCard title="HTML и CSS" price={300} />
<CourseCard title="JavaScript" price={400} />
<CourseCard title="React Lite" price={450} />
</div>
);
}
В каждую карточку добавляем собственное состояние liked. Число лайков не храним — вычисляем: likes + (liked ? 1 : 0). Нажмите на несколько сердечек: карточки не мешают друг другу.
А если нужен счётчик «понравилось N из M» над списком? Карточки не могут рассказать об этом соседям, а App не может заглянуть в состояние карточки. Решение — поднять состояние вверх: App хранит, какие курсы лайкнуты, а карточка получает liked и функциюonToggle через props. Да, props могут быть и функциями: так ребёнок «сообщает» родителю о событии.
Шаг 4 (бонус). Общий счётчик · Ctrl + Enter
// Шаг 4 (бонус). Поднимаем состояние: App знает, какие курсы лайкнуты, и считает общий счётчик
import { useState } from "react";
const courses = [
{ id: 1, title: "HTML и CSS" },
{ id: 2, title: "JavaScript" },
{ id: 3, title: "React Lite" },
];
// onToggle — это prop-ФУНКЦИЯ: карточка вызывает её, когда нажали на сердечкоfunctionCourseCard({ title, liked, onToggle }) {
return (
<div className="card">
<b>{title}</b>{" "}
<button onClick={onToggle}>{liked ? "♥ Нравится" : "♡ Лайк"}</button>
</div>
);
}
export defaultfunctionApp() {
const [likedIds, setLikedIds] = useState([]);
functiontoggle(id) {
setLikedIds(likedIds.includes(id) ? likedIds.filter((x) => x !== id) : [...likedIds, id]);
}
return (
<div>
<h3>Понравилось: {likedIds.length} из {courses.length}</h3>
{courses.map((course) => (
<CourseCard
key={course.id}
title={course.title}
liked={likedIds.includes(course.id)}
onToggle={() =>toggle(course.id)}
/>
))}
</div>
);
}
Староста группы. Каждый студент может сказать «я пришёл», но общий список посещаемости ведёт староста — один человек, у которого он есть целиком. Студент (карточка) не хранит у себя общую ведомость, а сообщает старосте (App) через функцию onToggle. А староста объявляет итог: «пришло 3 из 5».
Практика: собираем каталог
ЗАДАЧА 10.1
Каталог с лайками
средняя
Сейчас на экране одна заглушка. Соберите каталог по шагам 1–3: CourseCard получает props title и likes, App выводит все курсы через map, а у каждой карточки своё состояние liked: клик — ♥ и число на 1 больше, повторный клик — всё обратно.
Каталог с лайками · Ctrl + Enter
// Каталог курсов. Сейчас на экране одна заглушка. Соберите всё, что мы разобрали:// 1) CourseCard получает props title и likes и показывает название и кнопку «♡ число»;// 2) App выводит все курсы из массива через map с key;// 3) у каждой карточки своё состояние liked: клик → «♥ число+1», повторный клик → обратно «♡ число».const courses = [
{ id: 1, title: "HTML и CSS", likes: 10 },
{ id: 2, title: "JavaScript", likes: 25 },
{ id: 3, title: "React Lite", likes: 7 },
];
functionCourseCard() {
return (
<div className="card">
<b>Курс</b> <button className="like">♡ 0</button>
</div>
);
}
export defaultfunctionApp() {
return (
<div>
<CourseCard />
</div>
);
}
// ---- проверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
asyncfunctionclick(el) {
el.click();
awaitwait();
}
(async () => {
const cards = () => [...document.querySelectorAll(".card")];
const show = () =>
console.log(cards().map((c) => c.querySelector("b").textContent + " " + c.querySelector(".like").textContent).join(" | "));
awaitwait();
show();
awaitclick(cards()[0].querySelector(".like"));
awaitclick(cards()[2].querySelector(".like"));
show();
awaitclick(cards()[0].querySelector(".like"));
show();
})();
HTML и CSS ♡ 10 | JavaScript ♡ 25 | React Lite ♡ 7
HTML и CSS ♥ 11 | JavaScript ♡ 25 | React Lite ♥ 8
HTML и CSS ♡ 10 | JavaScript ♡ 25 | React Lite ♥ 8
Подсказка 1
Порядок работы: сначала props и map (проверьте, что видны три карточки), и только потом добавляйте useState в CourseCard.
Подсказка 2
В CourseCard: const [liked, setLiked] = useState(false);, на кнопке onClick={() => setLiked(!liked)} и {liked ? "♥" : "♡"} {likes + (liked ? 1 : 0)}. Не забудьте className="like" у кнопки и импорт useState.
11 Домашнее задание: Todo-лист на React
Todo-лист на React: добавить задачу, отметить выполненной, удалить. Артефакт — интерактивный компонент на useState + список через .map(). Основные приёмы вы уже потренировали в задачах 5.3 и 7.3 — теперь соберите их в одно приложение. Ниже стартовый код с «самопроверкой»: запустите его — в консоли появятся строки со знаками ✗, ваша цель — все ✓.
Todo-лист: стартовый код и самопроверка · Ctrl + Enter
// Todo-лист. Допишите состояние и три действия: добавить, отметить, удалить.// Блок «самопроверка» внизу сам нажимает кнопки и пишет в консоль ✓ или ✗ — добейтесь, чтобы все строки были с ✓.
import { useState } from "react";
export defaultfunctionApp() {
// состояние: массив задач { id, text, done } и текст из поляfunctionhandleSubmit(event) {
event.preventDefault();
// добавить задачу
}
return (
<div>
<form onSubmit={handleSubmit}>
<input placeholder="Новая задача" />{" "}
<button>Добавить</button>
</form>
<p id="left">Осталось: 0</p>
<ul>
{/* для каждой задачи: <li> с чекбоксом, <span> с текстом (класс done у выполненной) и кнопкой ✕ (className="del") */}
</ul>
</div>
);
}
// ---- самопроверка (не менять) ----const wait = () =>newPromise((r) =>setTimeout(r, 60));
functiontypeInto(el, value) {
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value").set.call(el, value);
el.dispatchEvent(newEvent("input", { bubbles: true }));
}
asyncfunctionadd(text) {
typeInto(document.querySelector("input[placeholder]"), text);
awaitwait();
document.querySelector("form").requestSubmit();
awaitwait();
}
(async () => {
const $ = (s) => document.querySelector(s);
const $$ = (s) => [...document.querySelectorAll(s)];
const check = (ok, msg) => console.log((ok ? "✓ " : "✗ ") + msg);
awaitwait();
awaitadd("Купить лепёшки");
awaitadd("Позвонить маме");
check($("input[placeholder]").value ==="", "поле очищается после добавления");
awaitadd(" ");
check($$("li").length ===2, "добавление: две задачи, пустая строка не добавилась");
check($("#left").textContent.trim() ==="Осталось: 2", "счётчик: Осталось: 2");
if ($$("li").length < 2) return console.log("✗ дальше проверять нечего: выведите задачи в список");
$$("li input[type=checkbox]")[0].click();
awaitwait();
check($$("li span")[0].classList.contains("done"), "отметка: у первой задачи класс done");
check(!$$("li span")[1].classList.contains("done"), "отметка: вторая задача не затронута");
check($("#left").textContent.trim() ==="Осталось: 1", "счётчик: Осталось: 1");
$$("li input[type=checkbox]")[0].click();
awaitwait();
check(!$$("li span")[0].classList.contains("done"), "повторный клик снимает отметку");
$$("li .del")[0].click();
awaitwait();
check($$("li").length ===1&& $$("li span")[0].textContent ==="Позвонить маме", "удаление: осталась «Позвонить маме»");
check($("#left").textContent.trim() ==="Осталось: 1", "счётчик после удаления");
})();
Состояние. Массив задач { id, text, done } и текст поля ввода (управляемое поле).
Добавление. Через <form onSubmit> с preventDefault. Пустые строки и строки из одних пробелов не добавляются. После добавления поле очищается. id создаётся при добавлении (Date.now()).
Список. Через .map() с key={task.id}. У каждой задачи: чекбокс, текст в <span> и кнопка ✕ с className="del".
Отметка. Чекбокс переключает done. У выполненной задачи у <span> класс done (зачёркнутый текст).
Удаление. Кнопка ✕ убирает задачу из списка.
Счётчик<p id="left">Осталось: N</p> — число невыполненных. Подсказка: это не отдельное состояние, а вычисляемое значение.
Условный рендер. Если задач нет — вместо списка показывается «Пока задач нет» (самопроверка к этому пункту не придирается, но на оценке он учитывается).
Никаких мутаций. Ни push, ни splice, ни присваиваний полям объектов в состоянии — только spread, map и filter.
Проверка ИИ. Если использовали ИИ — приложите свой промпт и ответ на вопрос: какую ошибку (мутация, ключ) ИИ допустил или чуть не допустил, и как вы это проверили.
Бонус для сильных
Фильтры «Все / Активные / Выполненные» — одно дополнительное состояние filter.
Кнопка «Очистить выполненные» — одна строка с filter.
Редактирование текста задачи по двойному клику.
Что сдавать
Ссылку на репозиторий в GitHub с проектом на Vite (App.jsx и стили).
Скриншот: список с тремя задачами, одна из них выполнена.
Короткий README: что умеет приложение и какие приёмы (props, map, useState) где использованы.
Как оцениваем (0–10)
Что
Баллы
Добавление: форма, preventDefault, пустые строки отсекаются, поле очищается
2
Отметка выполненной и счётчик «Осталось»
2
Удаление через filter
1
Список через .map() с key={id}, не индекс
2
Нет мутаций состояния (spread, map, filter)
2
Пустое состояние «Пока задач нет», чистый код, ИИ-код вы можете объяснить
1
Перед сдачей
Проверьте сами: добавьте три задачи, удалите среднюю и отметьте последнюю. Если зачёркнутой оказалась не та задача, значит, где-то остался индекс вместо id.
✓ Шпаргалка урока
Props: данные идут от родителя к ребёнку как атрибуты, приходят объектом: function Card({ name, price }). Props только для чтения. Строки — в кавычках, остальное — в { }.
children: всё между тегами компонента. Выводим {children} там, где нужно: рамки, панели, обёртки.
По умолчанию и rest:{ label = "Нажми" } срабатывает только для undefined; для null — ??. { a, ...rest } собирает остальное, <input {...rest} /> раздаёт.
Списки:items.map((x) => <Item key={x.id} … />). С { } после стрелки нужен return. Фильтр — filter перед map.
key: уникальный и стабильный, из данных (id). Не index для изменяемых списков (ломаются поля ввода и состояние строк) и не Math.random().
useState:const [x, setX] = useState(начальное). Начальное значение — только при первом показе. Обычная переменная экран не обновит. Хуки — только на верхнем уровне компонента.
Функциональное обновление:setX((x) => x + 1), когда новое значение зависит от старого. Вычисляемое не храним в состоянии.
Не мутируем: добавить [...arr, x], удалить arr.filter(...), изменить arr.map(...), объект { ...obj, k: v }. Нельзя: push, splice, obj.k = v, sort без копии.