Ментальные модели React делают его понятным: усвойте ключевые идеи о компонентах, рендеринге, состоянии и эффектах, а затем применяйте их, чтобы быстро собирать интерфейсы через чат.

React может раздражать сначала, потому что вы видите, как UI меняется, но не всегда можете объяснить, почему это произошло. Вы нажимаете кнопку, что‑то обновляется, а потом другая часть страницы вас удивляет. Обычно это не значит «React странный». Это значит «моя картина того, что делает React, расплывчата».
Ментальная модель — это простая история, которую вы рассказываете себе о том, как что‑то работает. Если история неверна, вы будете уверенно принимать решения, которые приводят к запутанным результатам. Подумайте о термостате: плохая модель — «я ставлю 22°C, и комната сразу становится 22°C». Лучшая модель — «я задаю цель, и обогреватель включает/выключает нагрев, чтобы достичь её со временем». С лучшей историей поведение перестаёт казаться случайным.
React работает так же. Как только вы примете несколько ясных идей, React станет предсказуемым: вы сможете посмотреть на текущие данные и с высокой вероятностью угадать, что будет на экране.
Dan Abramov помог популяризировать такой подход «сделай это предсказуемым». Цель не в том, чтобы заучить правила, а в том, чтобы держать в голове небольшой набор истин, чтобы отлаживать, рассуждая, а не методом проб и ошибок.
Имейте в виду эти идеи:
Держите это в голове, и React перестанет казаться магией. Он начнёт казаться системой, которой можно доверять.
React становится проще, когда вы перестаёте думать в терминах «экранов» и начинаете мыслить малыми кусочками. Компонент — это переиспользуемая единица UI. Он принимает входы и возвращает описание того, как UI должен выглядеть для этих входов.
Полезно думать о компоненте как о чистом описании: «при данных X показываем Y». Такое описание можно использовать в разных местах, потому что оно не зависит от того, где живёт компонент.
Props — это входы. Они приходят от родителя. Props не «принадлежат» компоненту, и компонент не должен тихо их менять. Если кнопке передали label="Save", задача кнопки — отрисовать эту метку, а не решать, что она должна быть другой.
State — это принадлежащие данные. Это то, что компонент запоминает во времени. State меняется, когда пользователь взаимодействует, когда завершается запрос или когда вы принимаете решение изменить что‑то. В отличие от props, state принадлежит этому компоненту (или тому компоненту, который вы выбрали владельцем).
Короткая версия ключевой идеи: UI — функция от state. Если state говорит «loading», показывайте спиннер. Если state говорит «error», показывайте сообщение. Если state говорит «items = 3», отрисуйте три строки. Ваша задача — держать UI читающим state, а не уходящим в скрытые переменные.
Быстрое разделение понятий:
SearchBox, ProfileCard, CheckoutForm)name, price, disabled)isOpen, query, selectedId)Пример: модалка. Родитель может передать title и onClose как props. Модалка может владеть isAnimating в state.
Даже если вы генерируете UI через чат (например, на Koder.ai), такое разделение всё равно — самый быстрый способ не сойти с ума: сначала решите, что будет props, а что state, а затем дайте интерфейсу следовать этому плану.
Полезная мысль о React (в духе Dan Abramov): рендер — это вычисление, а не покраска. React запускает функции ваших компонентов, чтобы понять, как UI должен выглядеть для текущих props и state. Результат — описание UI, а не пиксели.
Ререндер просто означает, что React повторяет это вычисление. Это не значит «весь сайт перерисовался». React сравнивает новый результат со старым и применяет минимальный набор изменений к реальному DOM. Многие компоненты могут ререндериться, тогда как реально обновится только пара узлов DOM.
Большинство ререндеров происходят по простым причинам: изменился state компонента, изменились его props или родитель перерендерился и React попросил ребёнка снова отрендериться. Последний пункт иногда удивляет, но обычно это нормально. Если вы будете считать рендер «дёшевым и скучным», приложение станет легче для понимания.
Правило большого пальца: делайте render чистым. При одинаковых входных данных (props + state) компонент должен возвращать одинаковое описание UI. Убирайте сюрпризы из render.
Конкретный пример: если вы генерируете ID через Math.random() внутри render, при ререндере он изменится, и, например, чекбокс потеряет фокус или элемент списка переинициализируется. Создавайте ID один раз (в state, через memo или вне компонента), и render станет стабильным.
Если запомнить одно предложение: ререндер — это «пересчитать, каким должен быть UI», а не «перестроить всё заново».
Ещё одна полезная модель: обновления state — это запросы, а не немедленные присваивания. Когда вы вызываете сеттер вроде setCount(count + 1), вы просите React запланировать рендер с новым значением. Если вы прочитаете state сразу после этого, вы можете увидеть старое значение, потому что React ещё не отрендерил.
Именно поэтому важны «малые и предсказуемые» обновления. Предпочитайте описывать изменение вместо того, чтобы хвататься за то, что вы думаете текущим значением. Когда следующее значение зависит от предыдущего, используйте форму апдейтера: setCount(c => c + 1). Это соответствует тому, как React работает: несколько обновлений могут помещаться в очередь и затем применяться по порядку.
Иммутабельность — другая половина картины. Не изменяйте объекты и массивы на месте. Создавайте новый объект с изменением. Тогда React может увидеть «значение новое», а вам легче отследить, что поменялось.
Пример: переключение todo. Безопасный подход — создать новый массив и новый объект todo для изменённого элемента. Рискованный — делать todo.done = !todo.done внутри существующего массива.
Также держите state минимальным. Частая ловушка — хранить значения, которые можно вычислить. Если у вас есть items и filter, не храните filteredItems в state. Вычисляйте это во время render. Чем меньше переменных state, тем меньше способов для рассинхронизации.
Простой тест для определения, что должно быть в state:
Если вы строите UI через чат (включая Koder.ai), просите изменения маленькими патчами: «Добавь один булев флаг» или «Обнови этот список иммутабельно». Малые и явные изменения держат генератор и ваш React‑код в согласии.
Рендер описывает UI. Эффекты синхронизируются с внешним миром. «Внешний» — это то, чем React не управляет: сетевые запросы, таймеры, браузерные API и иногда императивная работа с DOM.
Если что‑то можно вычислить из props и state, обычно это не должно жить в эффекте. Помещая это в эффект, вы добавляете второй шаг (render → run effect → set state → render). Эта лишняя ступень — место, где появляются мерцания, петли и баги «почему это устарело?».
Типичная путаница: у вас есть firstName и lastName, и вы храните fullName в state с помощью эффекта. Но fullName не является сайд‑эффектом. Его лучше вычислить в render, и тогда он всегда будет совпадать.
Как правило: вычисляйте UI‑значения в render (или с useMemo, если дорого), а эффекты используйте для «что‑то сделать» — fetch, таймеры, подписки, сохранение и т.п., а не для «разобраться, что показать».
Относитесь к массиву зависимостей как к: «Когда эти значения меняются — нужно заново синхронизироваться с внешним миром». Это не трюк для производительности и не место для подавления предупреждений.
Пример: если вы запрашиваете данные при изменении userId, userId должен быть в массиве зависимостей, потому что он должен триггерить синхронизацию. Если эффект использует token — включите и его, иначе можно будет выполнить запрос со старым токеном.
Хорошая проверка интуиции: если удаление эффекта только делает UI неправильным, то, вероятно, это не настоящий эффект. Если удаление прекратит таймер, отменит подписку или пропустит fetch — то, вероятно, это эффект.
Одна из самых полезных ментальных моделей проста: данные текут вниз по дереву, а действия пользователя — вверх.
Родитель передаёт значения детям. Дети не должны тайно «владеть» той же самой информацией в двух местах. Они должны запрашивать изменения, вызывая функцию, а родитель решает, каким будет новое значение.
Когда две части UI должны соглашаться, выберите одно место для хранения значения и передайте его вниз. Это и есть «lifting state». Может показаться лишней сантехникой, но это предотвращает гораздо худшую проблему: два состояния, которые расходятся, и необходимость править костылями, чтобы их держать синхронными.
Пример: поисковая строка и список результатов. Если инпут хранит свой query, а список хранит свой query, рано или поздно вы увидите «инпут показывает X, а список использует Y». Решение — держать query в родителе, передавать его обоим и давать инпуту onChangeQuery(newValue).
Поднимать state не всегда нужно. Если значение важно только внутри одного компонента — держите его там. Держать state близко к месту использования обычно делает код понятнее.
Практическая граница:
Если вы сомневаетесь, поднимайте state, когда видите признаки: два компонента показывают одно и то же по‑разному; действие в одном месте должно обновить что‑то далеко; вы копируете props в state «на всякий случай»; или вы добавляете эффекты только чтобы держать два значения синхронизированными.
Эта модель также помогает при сборке через чат‑инструменты вроде Koder.ai: попросите один владелец для каждого фрагмента общего состояния и генерируйте обработчики, которые поднимают изменения вверх.
Выберите фичу, достаточно маленькую, чтобы её удержать в голове. Хороший пример — список с поиском, где по клику на элемент открывается модалка с деталями.
Начните со схемы частей UI и событий, которые могут произойти. Пока не думайте о коде. Подумайте, что пользователь может делать и что он видит: есть поисковый инпут, список, подсветка выбранной строки и модалка. События: ввод в поиск, клик на элемент, открытие модалки, закрытие модалки.
Теперь «нарисуйте state». Запишите несколько значений, которые нужно хранить, и решите, кто ими владеет. Простое правило: ближайший общий родитель тех мест, которые нуждаются в значении, должен владеть им.
Для этой фичи состояние может быть крошечным: query (строка), selectedId (id или null), isModalOpen (булево). Список читает query и рендерит элементы. Модалка читает selectedId, чтобы показать детали. Если и список, и модалка пользуются selectedId, храните его в родителе, а не в обоих.
Далее отделите производные данные от хранимых. Отфильтрованный список — производное: filteredItems = items.filter(...). Не храните его в state, потому что его всегда можно пересчитать из items и query. Хранение производного — источник дрейфа значений.
Только потом спросите: нужен ли эффект? Если элементы уже в памяти — нет. Если ввод должен выполнять fetch — да. Если закрытие модалки должно что‑то сохранять — да. Эффекты — для синхронизации (fetch, save, subscribe), а не для базовой логики UI.
Наконец, протестируйте поток на парах кейсов:
selectedId?Если вы можете ответить на эти вопросы на бумаге, писать React‑код обычно просто.
Большая часть путаницы в React не в синтаксисе. Она возникает, когда код перестаёт соответствовать простой истории в вашей голове.
Хранение производного состояния. Вы сохраняете fullName в state, хотя это просто firstName + lastName. Это работает до тех пор, пока одно поле не изменится, а другое — нет, и UI покажет устаревшее значение.
Петли в эффектах. Эффект делает fetch, ставит state, а список зависимостей заставляет его снова выполняться. Симптом: повторяющиеся запросы, дергающийся UI или состояние, которое не стабилизируется.
Устаревшие замыкания. Обработчик клика читает старое значение (например, устаревший счётчик или фильтр). Симптом: «я кликнул, но он использовал вчерашнее значение».
Глобальное состояние везде. Кладёте все детали UI в глобальный стор — потом сложно понять, кто за что отвечает. Симптом: вы меняете одно, и реагируют три экрана непредсказуемо.
Мутация вложенных объектов. Обновляете объект или массив на месте и удивляетесь, почему UI не обновился. Симптом: «данные изменились, но ничего не перерендерилось».
Конкретный пример: панель «поиск и сортировка» для списка. Если вы храните filteredItems в state, оно может уйти от items, когда придут новые данные. Вместо этого храните входы (поиск, сорт) и вычисляйте список в render.
С эффектами держите их для синхронизации с внешним миром (fetch, подписки, таймеры). Если эффект занимается базовой логикой UI — часто ему не место в эффекте.
При генерации или редактировании кода через чат эти ошибки проявляются быстрее, потому что изменения приходят большими кусками. Хорошая привычка — формулировать запросы через владельцев: «Какой источник истины для этого значения?» и «Можем ли мы вычислить это вместо хранения?».
Когда UI ведёт себя непредсказуемо, редко виноват сам React. Чаще всего — слишком много state, в неправильных местах или выполняющий чужую работу.
Прежде чем добавлять ещё один useState, остановитесь и спросите:
Небольшой пример: поисковая строка, выпадающий фильтр, список. Если вы храните и query, и filteredItems в state, у вас теперь два источника истины. Вместо этого держите query и filter в state, а filteredItems выводите в render из полного списка.
Это важно и при быстрой работе через чат‑инструменты. Скорость — хорошо, но продолжайте спрашивать: «Мы добавили state или по ошибке добавили вычисляемое значение?» Если это вычисляемое — удалите state и вычисляйте его.
Небольшая команда делает админку: таблица заказов, пара фильтров и диалог для редактирования. Первый запрос часто звучит расплывчато: «Добавьте фильтры и всплывающее окно редактирования». Это кажется просто, но часто превращается в рассыпавшееся состояние.
Сделайте запрос конкретным, переводя его в состояние и события. Вместо «фильтры» назовите состояние: query, status, dateRange. Вместо «всплывашка редактирования» назовите событие: «пользователь нажимает Edit у строки». Затем решите, кто владеет чем (страница, таблица или диалог) и что можно вывести.
Примеры подсказок, которые сохраняют модель (они же хорошо работают в чат‑генераторах типа Koder.ai):
OrdersPage, который владеет filters и selectedOrderId. OrdersTable контролируется filters и вызывает onEdit(orderId).»visibleOrders из orders и filters. Не храните visibleOrders в state.»EditOrderDialog, который получает order и open. При сохранении вызывайте onSave(updatedOrder) и закрывайте диалог.»filters в URL, а не для вычисления строк.»После генерации или обновления UI быстро проверьте изменения: каждое состояние имеет одного владельца, производные значения не хранятся, эффекты нужны только для синхронизации с внешним миром (URL, сеть, storage), и события текут вниз как props, вверх как колбэки.
Когда состояние предсказуемо, итерация становится безопасной. Вы можете изменить макет таблицы, добавить фильтр или поправить поля диалога, не догадываясь, какое скрытое состояние внезапно сломается.
Скорость полезна, если приложение остаётся понятным. Самая простая защита — применять эти ментальные модели как чек‑лист перед написанием или генерацией UI.
Начинайте каждую фичу одинаково: выпишите нужный state, события, которые его меняют, и кто его владеет. Если вы не можете сказать «Этот компонент владеет этим состоянием, а эти события его меняют», скорее всего вы получите рассеянное состояние и неожиданные ререндеры.
Если вы строите через чат, начните с режима планирования. Опишите компоненты, форму состояния и переходы простым языком перед запросом кода. Например: «Панель фильтров обновляет состояние query; список результатов выводится из query; выбор элемента ставит selectedId; закрытие очищает его.» Как только это читается понятно, генерация UI становится механической.
Если вы пользуетесь Koder.ai (koder.ai) для генерации React‑кода, стоит сделать быструю проверку здравого смысла: один явный владелец для каждого значения state, UI выводится из state, эффекты только для синхронизации, и нет дублирующих источников истины.
Делайте итерации малыми шагами. Если хотите изменить форму состояния (например, несколько флагов → одно поле status), сначала снимите снапшот, попробуйте, и откатитесь, если модель стала хуже. При более глубоком ревью или перед передачей кода, экспорт исходников поможет ответить на реальный вопрос: сохраняет ли форма состояния историю UI?
Хорошая отправная модель: UI = f(state, props). Ваши компоненты не «правят DOM»; они описывают, что должно отображаться на экране для текущих данных. Если экран неверен — смотрите на state/props, которые это породили, а не на DOM.
Props — это входные данные от родителя; компонент должен считать их доступными только для чтения. State — это память, которой владеет компонент (или тот компонент, который вы выбрали как владельца). Если значение нужно разделять, поднимите его вверх и передайте вниз через props.
Ререндер означает, что React перезапускает функцию вашего компонента, чтобы вычислить следующее описание UI. Это не обязательно означает, что вся страница перерисована. React затем применяет к реальному DOM минимальный набор изменений.
Потому что обновления состояния планируются, а не выполняются немедленно. Если следующее значение зависит от предыдущего, используйте форму с апдейтером, чтобы не опираться на возможно устаревшее значение:
setCount(c => c + 1)Это корректно даже если несколько обновлений находятся в очереди.
Избегайте хранения всего, что можно вычислить из уже имеющихся входных данных. Храните входы, вычисляйте остальное во время render.
Примеры:
items, filtervisibleItems = items.filter(...)Так вы предотвратите рассинхронизацию значений.
Используйте эффекты, чтобы синхронизироваться с вещами, которыми React не управляет: запросы, подписки, таймеры, браузерные API или императивная работа с DOM.
Не используйте эффект только для вычисления значений UI из state — вычисляйте это в render (или с useMemo, если вычисление дорогое).
Думайте про массив зависимостей как про список триггеров: «когда эти значения изменятся, синхронизируйся заново». Включайте все реактивные значения, которые эффект читает.
Если что‑то опущено — вы рискуете получить устаревшие данные (например, старый userId или token). Если добавить лишнее, можно получить петли — часто это признак того, что работа эффекта должна быть в обработчиках или в render.
Если две части UI всегда должны совпадать, храните состояние в их ближайшем общем родителе, передавайте значение вниз и колбэки вверх.
Быстрый тест: если вы дублируете одно и то же значение в двух компонентах и пишете эффекты, чтобы «держать их синхронными», вероятно, этому значению нужен один владелец.
Обычно это происходит, когда обработчик «захватывает» старое значение из предыдущего рендера. Частые исправления:
setX(prev => ...)Если клик использует «вчерашнее» значение, подозревайте устаревшую замыкание.
Начните с маленького плана: компоненты, владельцы состояния и события. Генерируйте код малыми патчами (добавить одно поле state, добавить один обработчик, вычислить одно значение), а не большими перестроениями.
Если вы используете чат‑инструмент вроде Koder.ai, просите:
Так сгенерированный код будет соответствовать ментальной модели React.