React представляет собой JavaScript-библиотеку для построения интерфейсов из компонентов. Он декларативный: вы описываете, как UI должен выглядеть при текущих данных, а React сам вычисляет, какие изменения DOM приведут экран к этому виду.
Императивный подход: с обычным DOM вы находите узлы и меняете их шаг за шагом (el.textContent = ..., appendChild), и для каждого нового состояния нужен свой набор шагов.
Декларативный подход: компонент является функцией от пропсов и стейта и возвращает JSX; когда данные меняются, React вызывает его снова и обновляет только то, что отличается.
Компоненты: UI делится на небольшие переиспользуемые части со своей разметкой и логикой, которые комбинируются как функции.
Библиотека, а не фреймворк: роутинг, загрузку данных и сборку дают React Router, TanStack Query, Vite или фреймворк вроде Next.js.
Рендереры: та же модель работает в вебе (react-dom) и в мобильных приложениях (React Native).
ReactJunior
Основы React
JSX, компоненты, props, children и композиция, условный рендер, списки и key, однонаправленный поток данных, основы React 19
Закрыто вопросов
0 / 21
- 0Повторить
- 13Часто спрашивают
Вопросы
Virtual DOM представляет собой дерево обычных JavaScript-объектов (React-элементов), которое возвращают ваши компоненты. При каждом рендере React сравнивает новое дерево с предыдущим и применяет к реальному DOM только разницу.
Зачем он нужен: вы пишете UI так, будто при каждом изменении перерисовывается всё, а браузер получает минимальный набор DOM-операций.
Он не быстрее аккуратного ручного кода: написанное вручную обновление делает меньше работы; смысл в том, чтобы быть достаточно быстрым и при этом сохранить декларативный код.
Цена: React всё равно вызывает функции компонентов и сравнивает деревья, поэтому большое дерево с частыми ререндерами может тормозить; это решают memo, React Compiler и стейт, который лежит рядом с местом использования.
Сравнение эвристическое: элемент другого типа заменяет всё своё поддерево, а key подсказывает React, какие элементы списка соответствуют друг другу.
Терминология: в документации React теперь говорят «React-элементы» и «reconciliation» (согласование); Fiber служит внутренней структурой, которая выполняет эту работу.
JSX представляет собой расширение синтаксиса, которое позволяет писать разметку прямо в JavaScript. Компилятор (Babel, SWC, esbuild или TypeScript) превращает каждый тег в вызов функции, которая создаёт React-элемент: обычный объект с описанием того, что нужно отрендерить.
Automatic runtime (React 17+): <h1 className="t">Hi</h1> компилируется в jsx('h1', { className: 't', children: 'Hi' }) из react/jsx-runtime, поэтому в файлах больше не нужен import React.
Выражения в фигурных скобках: {user.name}, {items.length > 0 && ...}; только выражения, а не инструкции if или for.
Атрибуты становятся camelCase-пропсами: className, htmlFor, onClick, tabIndex; style принимает объект.
Один корень: компонент возвращает один элемент или Fragment <>...</> вокруг нескольких.
Каждый тег закрывается: <img />, <br />.
Экранирование по умолчанию: строка в фигурных скобках выводится как текст, поэтому <script> внутри неё никогда не выполнится.
Компонент представляет собой JavaScript-функцию, которая принимает пропсы и возвращает JSX. Имя пишут с заглавной буквы, потому что JSX считает тег со строчной буквы, например <button>, встроенным HTML-элементом, а тег с заглавной, например <Button>, переменной, которую нужно вызвать.
Что ломается: <profile /> компилируется в jsx('profile', ...) со строковым типом, и React создаёт неизвестный DOM-элемент <profile> вместо вызова вашей функции.
Стандарт сейчас: функциональные компоненты. Классовые компоненты с render() считаются легаси; они встречаются в старом коде и в error boundaries.
Объявляйте компоненты на верхнем уровне модуля, никогда внутри другого компонента: так на каждом рендере появляется новый тип и его стейт сбрасывается.
Компоненты вкладываются: компонент рендерит другие компоненты, и это дерево и есть ваш UI.
Классовый компонент наследует React.Component, хранит стейт в this.state и реагирует на монтирование и обновления в методах жизненного цикла; функциональный компонент является обычной функцией с хуками. По умолчанию пишут функциональные компоненты, а класс нужен только для error boundary.
Стейт: у класса один объект this.state, в который this.setState({ a }) вливает поля; useState хранит одно значение на вызов, и его сеттер заменяет это значение.
**this**: обработчикам в классе нужен bind или стрелочная функция в поле класса; у функционального компонента this нет.
Свежие пропсы или снимок: this.props всегда указывает на последние пропсы, поэтому таймаут, запущенный до ререндера, читает новые значения; обработчик функционального компонента хранит пропсы своего рендера.
Переиспользование логики: у классов для этого служат компоненты высшего порядка и render props, у функций кастомные хуки.
Error boundary требует static getDerivedStateFromError или componentDidCatch, для которых нет хука; react-error-boundary оборачивает такой класс за вас.
Классы по-прежнему работают, но React Compiler оптимизирует только функциональные компоненты и хуки.
Пропсы (props) представляют собой входные данные, которые родитель передаёт ребёнку, как аргументы функции. Они доступны только для чтения: данными владеет родитель, а при одинаковых пропсах компонент должен давать одинаковый результат, поэтому их изменение в ребёнке ломает единый источник истины.
Любые значения: строки, числа, объекты, массивы, функции (колбэки) и JSX.
Деструктуризация со значениями по умолчанию: function Button({ size = 'md', children }).
Чтобы изменить то, что показывает дочерний компонент, родитель рендерит его снова с новыми пропсами; чтобы ребёнок мог попросить изменение, родитель передаёт колбэк вроде onChange.
Мутация пропсов (props.items.push(x)) меняет объект родителя в обход React: ререндера не будет, а другие компоненты неожиданно увидят изменение.
Spread с осторожностью: {...rest} пробрасывает неизвестные пропсы в DOM-элемент; это удобно для обёрток, но так легко протащить невалидные атрибуты.
Пропсы компонент получает от родителя и не может их менять; стейт (состояние) принадлежит самому компоненту, и он меняет его через сеттер, например тот, что возвращает useState. Изменение и того, и другого заставляет React снова отрендерить компонент.
Кто владеет: пропсами владеет родитель, стейтом владеет сам компонент.
Кто меняет: пропсы меняются, только когда родитель ререндерится с новыми значениями; стейт меняется через setX, обычно в обработчике события.
Как они связаны: стейт одного компонента часто становится пропсами другого: родитель хранит selectedId в стейте и передаёт его вниз.
Что класть в стейт: только то, что меняется со временем и не вычисляется из пропсов или другого стейта; значение, производное от пропсов, считают во время рендера.
Снимок: и пропсы, и стейт зафиксированы на один рендер; новое значение появится на следующем рендере.
children содержит JSX, вложенный между открывающим и закрывающим тегами компонента, и приходит в компонент как проп. Компоненты переиспользуют друг друга через обёртывание и передачу JSX (композицию), а не через наследование классов, поэтому каждая часть остаётся независимой и легко заменяется.
Контейнеры, которые не знают своего содержимого: Card, Modal и Layout рендерят {children} в нужном месте.
Несколько слотов: JSX передают в именованных пропсах вроде header и footer рядом с children.
Специализация: DangerButton рендерит <Button variant="danger" {...props} /> вместо наследования от него.
Меньше prop drilling: родитель сам создаёт вложенный компонент и передаёт его внутрь, и промежуточный лейаут вообще не видит его пропсов.
Тип: children: ReactNode покрывает элементы, строки, числа, массивы, null и false.
Обычным JavaScript: if с ранним return до JSX, тернарный оператор cond ? <A /> : <B /> внутри JSX и cond && <A />, чтобы отрендерить что-то или ничего. Возврат null ничего не рендерит.
**if с ранним return: лучше всего, когда отличается вся ветка, например экран загрузки или ошибки.
Тернарный оператор: две альтернативы прямо в разметке; вкладывайте не больше одного уровня, иначе вынесите ветку в переменную или компонент.
&&**: только когда слева настоящий boolean; 0 && <X /> отрендерит 0.
Переменная: let content; if (...) content = <A />;, а затем {content} сохраняет сложный JSX читаемым.
Скрыть или убрать: условие размонтирует компонент и сбрасывает его стейт; CSS-атрибут hidden оставляет его смонтированным.
Массив превращают в JSX через map: items.map(item => <Row key={item.id} ... />). key подсказывает React, какой элемент каким был между рендерами, поэтому при добавлении, удалении или перестановке он двигает нужные DOM-узлы и сохраняет стейт каждого элемента вместе с ним.
Уникален среди соседей и стабилен между рендерами: идеально подходит id из данных.
**Никогда не Math.random(): новый ключ на каждом рендере перемонтирует все строки, и они теряют стейт и фокус.
Индекс в качестве ключа допустим только для статичного списка, который никогда не сортируют, не фильтруют и в который не вставляют элементы.
Без ключа React выдаёт предупреждение («Each child in a list should have a unique "key" prop») и использует индекс.
key не является пропсом**: дочерний компонент не может прочитать props.key; если id нужен, передайте его отдельно.
Ключ ставят на самый внешний элемент, который возвращает map, в том числе на <Fragment key={...}>.
Fragment, который пишут как <>...</> или <Fragment>, группирует несколько элементов, не добавляя DOM-узел. Он нужен, когда компонент должен вернуть несколько соседних элементов, ведь компонент возвращает один корень.
Валидный HTML: компонент, который возвращает ячейки <td> для <tr> или пары <dt> и <dd> для <dl>, не может обернуть их в <div>.
Вёрстка: лишняя обёртка ломает flex- и grid-раскладку и селекторы вроде ul > li.
Ключи: короткий синтаксис не принимает атрибутов; внутри map пишут <Fragment key={id}>.
Массив тоже работает, но тогда каждому элементу нужен свой ключ, и код получается шумнее.
Берут фреймворк вроде Next.js или React Router, когда нужны роутинг, загрузка данных и SSR, или Vite для чисто клиентского приложения. create-react-app объявили устаревшим в феврале 2025 года: он не поддерживался, работал медленно и не решал роутинг, загрузку данных и code splitting.
Vite: npm create vite@latest my-app -- --template react-ts; dev-сервер на нативных ES-модулях, который стартует мгновенно, и оптимизированная продакшен-сборка.
Next.js: App Router, React Server Components, серверный рендеринг и роутинг на файлах; npx create-next-app@latest.
React Router v7 во framework mode, наследник Remix: npx create-react-router@latest.
Существующие CRA-приложения продолжают работать, но не получают обновлений; обычно их переносят на Vite.
TypeScript по умолчанию: у каждого шаблона есть TS-вариант, и большинство команд начинают с него.
При пустой корзине под заголовком появится лишний 0. Выражение items.length && ... даёт число 0, а React рендерит числа как текст; он пропускает только false, true, null и undefined.
**Как работает &&**: если левый операнд ложный, оператор возвращает сам этот операнд, а не false, поэтому всё выражение равно 0.
**Почему виден 0**: React считает дочернее число текстовым узлом, точно так же как {count}.
Исправление: сделайте левую часть boolean, items.length > 0 && ..., или используйте тернарный оператор с null для пустой ветки.
В React Native тот же код падает с ошибкой «Text strings must be rendered within a <Text> component», так что там баг заметнее.
Список использует индекс массива как key. После удаления первого элемента второй получает индекс 0, поэтому React считает его старой первой строкой и сохраняет этот экземпляр Row вместе со стейтом note.
Что видит React: ключи 0, 1, 2 стали 0, 1; он передаёт новые пропсы title строкам 0 и 1 и удаляет строку 2, то есть последнюю.
Стейт живёт в экземпляре компонента, а React сопоставляет экземпляры по типу, позиции и ключу, а не по данным, которые вы имели в виду.
Исправление: стабильный id из данных, key={item.id}; тогда удаляется строка с удалённым id, а остальные сохраняют свой стейт.
Индекс в ключе безопасен только для списков, которые никогда не сортируют, не фильтруют и в которые не вставляют элементы.
Данные текут сверху вниз: родитель передаёт значения детям через пропсы, и дети не могут их менять. Чтобы сообщить что-то наверх, дочерний компонент вызывает колбэк, который родитель передал пропсом, а родитель обновляет свой стейт, и новые данные снова текут вниз.
Единый источник истины: у каждого кусочка стейта один владелец, поэтому всегда понятно, откуда пришло значение и кто может его менять.
Колбэки тоже идут вниз: onSelect, onChange, onClose; ребёнок сообщает, что произошло, а родитель решает, что делать.
Соседи делятся данными через ближайшего общего родителя, который хранит стейт (подъём состояния, lifting state up).
В отличие от двустороннего связывания (Angular [(ngModel)], Vue v-model) React делает обновление явным через value и onChange.
Глубокие деревья: когда проп проходит через много слоёв, используют композицию или Context; поток при этом остаётся однонаправленным.
React может вызвать компонент много раз, в любом порядке или выбросить рендер (StrictMode, конкурентный рендеринг), поэтому рендер только вычисляет JSX из пропсов, стейта и контекста. Изменение всего, что существовало до рендера, считается побочным эффектом: ему место в обработчике или эффекте.
Чистый компонент: одинаковые входные данные дают одинаковый JSX, и ничего за пределами функции не меняется.
Побочные эффекты в рендере: мутация пропсов, стейта или переменной модуля, запись в localStorage, вызов fetch, установка document.title, подписка на события.
Локальная мутация допустима: массив, созданный внутри рендера, можно наполнять через push, наружу это не протекает.
StrictMode в разработке вызывает компоненты дважды, чтобы выявить нечистый код: удвоенный результат показывает баг.
Почему это окупается: React может пропускать, приостанавливать и перезапускать рендер, а React Compiler может мемоизировать автоматически только потому, что компоненты чистые.
SearchBox объявлен внутри SearchPage, поэтому каждый рендер создаёт новую функцию, и React видит в ней новый тип компонента. Он размонтирует старый input и монтирует новый, а у нового input нет фокуса.
Идентичность типа: React сравнивает типы элементов через ===; функция, созданная во время рендера, никогда не равна предыдущей.
Перемонтирование вместо обновления: DOM-узел заменяется, и любой стейт внутри вложенного компонента сбрасывается при каждом нажатии.
Исправление: вынесите компонент на верхний уровень модуля и передайте ему нужное через пропсы.
Линтер это ловит: в eslint-plugin-react есть правило react/no-unstable-nested-components.
Row перестаёт быть компонентом: React его не видит, поэтому его useState становится хуком List, по одному на каждый элемент. Как только в список добавляется элемент, React бросает «Rendered more hooks than during the previous render», и страница становится пустой.
Добавление и удаление элементов меняют число хуков, которые вызывает List; при удалении падает «Rendered fewer hooks than expected». Без error boundary размонтируется весь корень.
Стейт привязан к позиции, а не к элементу: флаги open лежат в списке хуков List по индексу, поэтому после перестановки открытой оказывается не та строка.
Нет ключа: <li> возвращается без key, и React предупреждает «Each child in a list should have a unique "key" prop».
Ререндер всего списка: setOpen в любой строке перерисовывает весь List. memo тоже не поможет: вызов memo-компонента бросает TypeError: Row is not a function, потому что memo возвращает объект.
Исправление: отрендерить элемент, <Row key={item.id} item={item} />, и React создаст экземпляр на каждую строку с собственным стейтом.
Обычный вызов допустим для хелпера без хуков, например renderPrice(cents); дайте ему имя со строчной буквы, чтобы его не приняли за компонент.
componentDidMount, componentDidUpdate и componentWillUnmount сливаются в один useEffect с функцией очистки, shouldComponentUpdate превращается в memo, а стейт из конструктора переходит в useState. У getSnapshotBeforeUpdate, getDerivedStateFromError и componentDidCatch хука нет.
**constructor**: начальный стейт уходит в useState или его ленивую форму useState(() => compute()); привязывать ничего не нужно.
Монтирование, обновление, размонтирование: один эффект покрывает все три: настройка после монтирования, очистка и повторная настройка при изменении зависимости, очистка при размонтировании. Классу нужна была проверка prevProps.roomId !== this.props.roomId, и забыть её было классической ошибкой.
Время запуска: componentDidMount и componentDidUpdate срабатывают до отрисовки, как useLayoutEffect; useEffect обычно выполняется после отрисовки, и это важно только при замерах DOM.
**shouldComponentUpdate**: memo(Component, arePropsEqual), но с обратным смыслом: arePropsEqual возвращает true, чтобы пропустить рендер.
**getDerivedStateFromProps**: вычислите значение во время рендера или сбросьте стейт через key.
Без хука: getSnapshotBeforeUpdate (чтение позиции прокрутки прямо перед изменением DOM) и error boundary; документация React советует оставить для них класс.
React 19 (декабрь 2024) сделал ref обычным пропсом, поэтому forwardRef больше не нужен, и добавил Actions с useActionState, useFormStatus и useOptimistic, API use, <Context> в роли провайдера и поддержку <title> и <meta> внутри компонентов.
**ref как проп**: функциональные компоненты получают ref, как любой другой проп; forwardRef пока работает, но уходит.
Actions: асинхронную функцию передают в <form action={...}> или startTransition, и React сам отслеживает состояние ожидания, ошибки и сброс формы.
**use(promise) и use(context)**: чтение промиса внутри Suspense или контекста; в отличие от хуков, use можно вызывать по условию.
**<ThemeContext value="dark">** заменяет <ThemeContext.Provider>.
Метаданные документа: <title>, <meta> и <link>, отрендеренные где угодно, поднимаются в <head>.
Ref-колбэки могут возвращать функцию очистки.
React Compiler 1.0 (октябрь 2025) представляет собой отдельный плагин сборки, который мемоизирует компоненты автоматически, поэтому ручные useMemo и useCallback нужны гораздо реже.
React привязывает стейт к позиции компонента в дереве. Стейт сохраняется, пока на той же позиции рендерится тот же тип компонента с тем же key, и сбрасывается, когда меняется тип на этой позиции, компонент удаляется или меняется его key.
Та же позиция, тот же тип: {isAdmin ? <Form /> : <Form />} сохраняет стейт, потому что обе ветки дают один и тот же Form на одном месте.
Другой тип: замена обёртки <div> на <section> или <Form> на <Wizard> уничтожает стейт всего поддерева.
Удалили и добавили снова: {show && <Form />} сбрасывает форму при каждом новом появлении.
**key как идентичность**: <ChatInput key={contactId} /> даёт каждому контакту свой экземпляр, поэтому при переключении контакта черновик очищается без всякого эффекта сброса.
Лучше эффекта, который сбрасывает стейт при смене пропса: эффект сначала рендерит устаревшее значение, а потом рендерит ещё раз.
Чтобы сохранить стейт при удалении, поднимите его выше или храните вне компонента, например в Map по id или в сторе.