React 19 для разработчиков Vue 3, часть 2: React повторно выполняет функцию вашего компонента для рендеринга; Vue отслеживает зависимости и обновляет их на месте.
В первой части этой серии мы пересмотрели ментальную модель: компонент React — это просто функция, JSX — это JavaScript с разметкой, пропсы — это первый аргумент, а children — это ваш слот по умолчанию. Я закончил ту статью одним предложением, которое просил вас запомнить, и теперь пришло время вернуться к нему: пропсы в React не являются реактивными в том же смысле, что и пропсы во Vue, потому что вся функция компонента выполняется заново для создания следующего рендера.
Этот единственный факт является ключом ко всей статье. Почти каждая особенность React, которая раздражает разработчиков Vue (состояние, которое кажется обновляющимся «на шаг позже», значения, которые загадочным образом заморожены внутри обратного вызова, эффекты, которые срабатывают чаще, чем вы ожидали), сводится к одной и той же первопричине. Поэтому, прежде чем мы коснемся хоть одного хука, давайте по-настоящему усвоим, как React принимает решение о повторном рендеринге, потому что это действительно отличается от Vue.
Изображение создано с помощью ИИ
Основное различие: Vue отслеживает, React выполняет повторно
Во Vue реактивность является гранулярной и автоматической. Когда вы создаете ref и используете его в шаблоне, Vue незаметно записывает эту зависимость. Когда вы позже меняете .value, Vue точно знает, какие части DOM зависят от него, и обновляет только их. Ваш setup выполняется один раз. Реактивная система берет на себя всю дальнейшую работу.
React не отслеживает зависимости таким образом. Когда часть состояния меняется, React заново выполняет всю функцию вашего компонента с самого начала, создает новое описание пользовательского интерфейса, сравнивает его с предыдущим и применяет различия к DOM. Тело вашего компонента — это не разовая настройка; это функция рендеринга, которая выполняется снова и снова.
Удерживайте этот образ в голове, потому что он объясняет все нижесказанное. Во Vue вы пишете код, который один раз настраивает реактивные связи. В React вы пишете код, который описывает пользовательский интерфейс для текущих значений, и он выполняется при каждом рендеринге.
useState против ref
Давайте начнем с самого частого действия: локального состояния компонента. Во Vue вы используете ref (или reactive), читаете и записываете через .value, а Vue берет на себя все остальное.
Counter.vue
В React эквивалентом является хук useState. Он возвращает пару: текущее значение и функцию для его установки. Соглашение об именовании — [thing, setThing], и вы почти всегда деструктурируете его прямо при объявлении.
Counter.jsx
Давайте разберем это. useState(0) объявляет одну часть состояния с начальным значением 0. Он возвращает count (обычное число, не объект-обертку, поэтому никакого .value) и setCount (единственный одобренный способ изменить его). Чтение состояния — это просто чтение переменной. Запись состояния означает вызов сеттера.
Этот последний пункт — главное поведенческое различие. Во Vue вы мутируете: count.value++. В React вы не мутируете, вы заменяете: вы вызываете setCount с новым значением, и React планирует повторный рендеринг. Не существует count = count + 1, и нет никакого count.value. На самом деле, вы вообще не можете переназначить count; это const, поэтому такая строка просто вызовет ошибку. И даже если бы это был обычный let, изменение переменной не обновило бы экран, потому что React выполняет повторный рендеринг только при вызове сеттера.
Заметьте, что здесь также нет глубокой реактивности. count — это снимок для данного рендера (в собственной документации React это называется моделью «состояние как снимок», и эту страницу стоит прочитать, как только придет понимание). Когда вы вызываете setCount, React не обновляет текст кнопки хирургически, как это сделал бы Vue. Он выполняет Counter с нуля, useState возвращает новое значение, и возвращенный JSX отражает его. Тот же видимый результат, совершенно другой механизм.
Обновления состояния — это снимок, а не «живое» значение
Вот первый момент, на котором люди спотыкаются, и его стоит увидеть заранее. Предположим, вы хотите увеличить значение дважды в одном обработчике:
Придя из Vue, вы бы поставили деньги на то, что это добавит 2. Сюрприз, это добавит 1!
Причина в том, что count — это константа, зафиксированная для конкретного рендера. Оба вызова читают один и тот же count, вычисляют один и тот же count + 1 и устанавливают одно и то же значение. React не обновляет count на месте между двумя строками, потому что count — это просто число из снимка этого рендера.
Решение — функциональная форма обновления сеттера, которая передает вам последнее ожидающее значение:
Теперь каждый вызов получает самое актуальное значение и возвращает следующее, поэтому это добавит 2. Мое эмпирическое правило: как только ваше новое состояние зависит от предыдущего, используйте функциональную форму. Во Vue вы никогда не думаете об этом, потому что count.value — это всегда актуальное «живое» значение. В React значение заморожено для каждого рендера, поэтому вы явно запрашиваете у React самое свежее.
useEffect против watchEffect, watch и onMounted
Далее: выполнение побочных эффектов. Во Vue у вас есть небольшое семейство инструментов. onMounted для «сделать это один раз после монтирования компонента», watchEffect для «выполнить это и повторить, когда изменятся реактивные зависимости» (отслеживаются автоматически), и watch для «выполнить это, когда изменятся конкретные источники». Очистка происходит через onUnmounted или обратный вызов очистки внутри наблюдателя.
React объединяет большую часть этого в один хук, useEffect, с одним важным нюансом: он не отслеживает зависимости за вас. Вы сами перечисляете их в массиве, и этот массив — ручная версия автоматического отслеживания Vue.
Давайте получим пользователя при изменении пропса id, сравнив подходы.
UserProfile.vue
UserProfile.jsx
Давайте разберем это, потому что здесь много соответствий.
- Тело эффекта — это то, что выполняется, так же как ваш обратный вызов watch. Обратите внимание, что обратный вызов useEffect сам по себе не может быть асинхронным (он может возвращать только функцию очистки или ничего), поэтому мы определяем внутреннюю асинхронную функцию и вызываем ее. Это небольшая идиома React, которую вы будете часто повторять.
- Массив зависимостей [id] — это ручной источник watch. Он говорит React: «повторно выполняй этот эффект всякий раз, когда id меняется между рендерами». Vue вычислял эту зависимость за вас из геттера; в React вы указываете ее сами.
- { immediate: true } в наблюдателе Vue не имеет отдельного переключателя в React. Эффекты всегда выполняются после первого рендеринга, поэтому «выполнить при монтировании» — это поведение по умолчанию, а не опция.
Вот краткая таблица перевода для массива зависимостей (часть с [id]), так как именно она сбивает всех с толку. Я начал со случая, который больше всего волнует разработчика Vue:
- Пустой массив []: Выполняется один раз после первого рендеринга. Это ваш onMounted.
- Заполненный массив, например [id]: Выполняется после первого рендеринга и снова при любом рендеринге, где id отличается от предыдущего. Это ваш watch для конкретных источников.
- Массив отсутствует: Выполняется после каждого рендеринга. Вам почти никогда это не нужно.
Одна честная сноска по первому случаю: onMounted во Vue выполняется после того, как DOM готов, но до того, как браузер отрисует его, в то время как useEffect выполняется после отрисовки. Для получения данных или установки фокуса на поле ввода разница незаметна. Но если вам нужно измерить макет до того, как пользователь его увидит, useLayoutEffect — это более близкий аналог onMounted.
Очистка работает так же, как функция очистки (cleanup) в наблюдателе (watcher) Vue. Vue повторно запускает эту функцию перед следующим выполнением и при размонтировании, а возвращаемая функция в React работает точно так же: она срабатывает перед следующим запуском эффекта и последний раз при уничтожении компонента. Именно здесь вы очищаете интервалы, удаляете слушатели или прерываете запросы.
Эта возвращаемая стрелочная функция — ваш onUnmounted. Та же идея, что и функция очистки в watch, та же причина: не оставляйте таймеры и слушатели висеть в памяти.
Однако будьте осторожны! Массив зависимостей — это самый главный источник багов для новичков в React, и ошибка всегда одна и та же: что-то забыли указать. Если ваш эффект читает значение из пропсов или состояния, это значение должно быть в массиве. Его опускание не заставляет эффект «работать реже». Оно заставляет эффект работать со старым значением, что плавно подводит нас к ловушке, в которую попадаются все разработчики Vue.
Ловушка устаревшего замыкания (Stale-Closure Trap)
Эту мысль я хочу, чтобы вы запечатлели где-нибудь на видном месте, потому что во Vue такого нет, и первые несколько раз это будет вас путать.
Помните, что функция компонента перезапускается при каждом рендере, и каждая функция, которую вы определяете внутри нее (обработчики событий, колбэки эффектов, таймауты), захватывает (closes over) переменные из этого конкретного рендера. Эти переменные являются константами. Они никогда не меняются после этого рендера. Поэтому, если колбэк переживает рендер, который его создал, он все равно смотрит на старые значения.
Вот классический сломанный вариант:
Timer.jsx
Вы ожидаете, что счетчик будет бесконечно увеличиваться. Он доходит до 1, а затем останавливается. Эффект выполнился один раз (пустой массив), поэтому колбэк интервала захватил count из самого первого рендера, где count был равен 0. Каждый тик вычисляет 0 + 1. Значение заморожено в замыкании. (В режиме разработки с включенным StrictMode в React вы также можете заметить, что эффекты выполняют дополнительный цикл монтажа и последующей очистки. Это сделано намеренно, и это не тот баг, который мы здесь ищем.)
Во Vue этот баг просто не может возникнуть, потому что count.value всегда читает актуальное значение через ref. Нет снимка (snapshot), который мог бы устареть. В React у вас есть два чистых решения:
- Используйте функциональное обновление, чтобы вообще не читать устаревший count: setCount((c) => c + 1). Это правильное решение в данном случае.
- Добавьте значение в массив зависимостей ([count]), чтобы эффект очищался и заново подписывался с новым замыканием при каждом его изменении. Это правильно, но для интервала это означает сброс таймера на каждом тике, поэтому форма с функцией обновления лучше.
Этот урок применим и в целом: всякий раз, когда значение внутри долгоживущего колбэка выглядит «неправильным» или «старым» в React, вы почти наверняка имеете дело с устаревшим замыканием. В девяноста процентах случаев решением является либо функциональное обновление, либо честный массив зависимостей.
useRef для значений, которые не должны вызывать повторный рендер
Последняя часть. Иногда вам нужно мутируемое значение, которое сохраняется между рендерами, но не должно вызывать повторный рендер при изменении. Представьте ссылку на DOM-узел, ID интервала или флаг «выполнено ли это уже». Во Vue ссылками на шаблоны (template refs) решается задача с DOM, а для нереактивных значений вы просто используете обычную переменную за пределами реактивного состояния (или ref, от которого сознательно никогда не зависят).
Инструмент React для обеих задач — useRef. Он возвращает объект с единственным свойством current, которое вы можете свободно читать и мутировать, причем его мутация никогда не вызывает рендер.
TextInput.vue
TextInput.jsx
Заметьте, насколько они похожи. Вы создаете ref, прикрепляете его к элементу с помощью атрибута ref и читаете его после монтажа. Единственная реальная разница — это .value во Vue против .current в React, а также тот факт, что в React вы обращаетесь к DOM внутри эффекта (потому что узел не существует до рендера).
То, что стоит подчеркнуть, — это вторая задача useRef. Представьте себе мутируемый контейнер, который переживает рендеры, не вызывая их. Если бы вы написали let x = 0 прямо в теле компонента, оно бы сбрасывалось в 0 при каждом рендере, потому что вся функция запускается заново. Если бы вы использовали useState, его изменение вызвало бы рендер. useRef — это нечто среднее: он сохраняет свое значение между рендерами, как состояние, но запись в .current происходит тихо, как в обычную переменную. Это именно то, что вам нужно для вещей, которые пользователь не видит напрямую, например для хранения ID таймера или предыдущего значения пропса.
Подведение итогов
Главное, что вам обязательно нужно вынести из этого чтения, заключается в том, что React повторно запускает функцию вашего компонента для рендеринга, тогда как Vue отслеживает зависимости и обновляет их на месте. Как только вы это поймете, остальное станет понятно.
useState дает вам значение плюс функцию установки. Значение — это снимок для каждого рендера, поэтому вы заменяете, а не мутируете, и вы используете функциональное обновление, когда новое состояние зависит от старого. useEffect — это ваши watch, watchEffect и onMounted, объединенные вместе, за исключением того, что вы перечисляете зависимости вручную и возвращаете функцию очистки. А useRef — это ваш запасной выход для мутируемых значений, которые не должны вызывать рендер, включая DOM-узлы.
Для получения более подробной информации документация React по состоянию и руководство «Вам может не понадобиться эффект» превосходны, причем последнее убережет вас от использования useEffect там, в котором вы на самом деле не нуждаетесь.
В следующий раз мы поднимемся на уровень выше и рассмотрим серверные компоненты (Server Components), новый API use и Actions, сопоставив их с ментальной моделью SSR и загрузки данных во Vue, которая у вас уже есть.
Приятного управления состоянием!









