Олег Марков
Реактивные переменные - концепция reactive и практические примеры
Введение
Реактивные переменные (reactive) — это переменные, изменение которых автоматически приводит к обновлению всех зависимых от них частей приложения. Вам не нужно вручную вызывать функции обновления интерфейса, синхронизировать несколько значений или следить, что вы где‑то забыли перерисовать компонент — фреймворк делает это за вас.
Смотрите, идея очень простая: вы объявляете значение и говорите системе — «следи за ним и везде, где оно используется, при изменении пересчитай результат». На этом строится современный frontend (Vue, Svelte, Solid, часть возможностей React), а также многие решения в backend и dataflow‑системах (RxJS, MobX и т.п.).
В этой статье я покажу вам:
- что такое реактивность концептуально;
- чем реактивные переменные отличаются от «обычных»;
- как это реализовано во Vue (ref, reactive, computed);
- как похожая идея выглядит в Svelte и React;
- какие подводные камни есть у реактивного подхода;
- как не превратить код в хаос из «магии обновлений».
Будем опираться на практические примеры и параллельно разбирать, что происходит под капотом.
Что такое реактивные переменные
Основная идея реактивности
Если упростить, реактивная переменная — это:
- хранилище значения;
- к которому можно «подписаться»;
- и которое уведомляет подписчиков при каждом изменении.
Давайте разберемся на очень абстрактном примере — без привязки к конкретному фреймворку:
// Псевдореализация простой реактивной переменной
function createReactive(initialValue) {
let value = initialValue // Текущее хранимое значение
const subscribers = new Set() // Набор подписчиков
function get() {
// Здесь можно было бы регистрировать зависимость
return value // Просто возвращаем текущее значение
}
function set(newValue) {
if (Object.is(newValue, value)) {
return // Если значение не изменилось - ничего не делаем
}
value = newValue // Обновляем значение
subscribers.forEach(fn => fn(value)) // Уведомляем всех подписчиков
}
function subscribe(fn) {
subscribers.add(fn) // Добавляем подписчика
return () => subscribers.delete(fn) // Возвращаем функцию отписки
}
return { get, set, subscribe }
}
// Здесь мы создаем реактивную переменную count
const count = createReactive(0)
// Подписываемся на изменения переменной count
const unsubscribe = count.subscribe(v => {
console.log("Новое значение count:", v)
})
// Устанавливаем новое значение - сработает подписчик
count.set(1) // Выведет "Новое значение count: 1"
// Отписываемся - больше не получаем обновления
unsubscribe()
count.set(2) // Подписчики не вызываются
Как видите, в основе всегда одна и та же идея: слежение за изменениями и обновление зависимостей. Разные фреймворки по‑разному реализуют это слежение и обновление, но суть остается неизменной.
В чем отличие от обычных переменных
Обычные переменные:
- просто хранят значение;
- никак не связаны с интерфейсом;
- при изменении ничего сами не инициируют.
Реактивные переменные:
- знают, кто от них зависит;
- автоматически запускают пересчет зависимых выражений/компонентов;
- часто имеют «обертку» (объект, proxy, getter/setter), а не являются «голым» значением.
Поэтому частое правило: доступ к реактивной переменной чуть менее «прямой», чем к обычной, зато вы получаете автоматическое обновление всего, что от нее зависит.
Реактивность во Vue 3 - ref, reactive, computed
Vue 3 — один из самых наглядных примеров реактивности, потому что он использует и примитивные реактивные переменные (ref), и реактивные объекты (reactive), и вычисляемые значения (computed). Здесь я покажу вам, как это выглядит на практике.
ref - реактивная обертка над значением
ref — это способ сделать реактивным примитивное значение (число, строку, boolean и т.п.) или вообще любое значение.
import { ref } from "vue"
export default {
setup() {
// Здесь мы создаем реактивную переменную count
const count = ref(0)
// Функция увеличения счетчика
function increment() {
// Обратите внимание - меняем count через .value
count.value++
}
return {
count, // Передаем во view
increment, // Экспортируем функцию
}
},
}
В шаблоне:
<template>
<!-- Vue автоматически "разворачивает" .value -->
<p>Текущее значение - {{ count }}</p>
<button @click="increment">Увеличить</button>
</template>
Ключевые моменты:
- внутри setup вы обращаетесь к значению ref через
.value; - в шаблоне Vue сам подставляет
.value, и вы пишете просто{{ count }}; - при изменении
count.valueVue понимает, какие части шаблона зависят от этого значения, и обновляет только их.
reactive - реактивный объект
Когда значение сложное (объект, массив), удобнее использовать reactive — он создает Proxy поверх объекта и делает все его поля реактивными.
import { reactive } from "vue"
export default {
setup() {
// Здесь мы создаем реактивный объект state
const state = reactive({
count: 0,
user: {
name: "Alex",
},
items: [],
})
function increment() {
// Для reactive не нужен .value
state.count++
}
function addItem(name) {
// Здесь добавляем новый элемент в массив items
state.items.push({ name })
}
return {
state,
increment,
addItem,
}
},
}
В шаблоне:
<template>
<p>Счетчик - {{ state.count }}</p>
<p>Пользователь - {{ state.user.name }}</p>
<ul>
<li v-for="item in state.items" :key="item.name">
{{ item.name }}
</li>
</ul>
<button @click="increment">+1</button>
<button @click="addItem('Новый элемент')">Добавить</button>
</template>
Как это работает под капотом:
- Vue оборачивает объект в Proxy;
- когда вы читаете
state.count, Vue регистрирует зависимость текущего эффекта (компонента, вычисляемого свойства и т.п.); - когда вы изменяете
state.count, Vue запускает все эффекты, которые зависят от этого поля.
computed - реактивные вычисляемые значения
Часто вам нужно хранить не только «сырые» данные, но и производные — например, отфильтрованный список, форматированную строку, сумму двух значений. Вычисляемые значения (computed) — это реактивные переменные, которые зависят от других реактивных переменных.
import { ref, computed } from "vue"
export default {
setup() {
// Здесь мы создаем реактивную переменную price
const price = ref(100)
// Здесь мы создаем реактивную переменную quantity
const quantity = ref(2)
// Здесь мы создаем вычисляемое значение total
const total = computed(() => {
// total автоматически перелсчитывается
// когда меняется price.value или quantity.value
return price.value * quantity.value
})
return {
price,
quantity,
total,
}
},
}
В шаблоне:
<template>
<p>Цена - {{ price }}</p>
<p>Количество - {{ quantity }}</p>
<p>Итого - {{ total }}</p>
</template>
Важно:
- computed кэшируется — функция пересчитывается только при изменении зависимостей;
- в отличие от обычной функции
total(), computed знает, от каких реактивных значений он зависит.
Когда использовать ref, а когда reactive
Небольшие правила, которыми удобно пользоваться:
- ref — для примитивов (чисел, строк, boolean) и единичных значений;
- reactive — для объектов и структур состояния (state) с несколькими полями;
- computed — для любых значений, которые можно вычислить из других реактивных переменных.
Смотрите, вот типичный пример смешанного подхода:
import { ref, reactive, computed } from "vue"
export default {
setup() {
// Примитив - ref
const search = ref("")
// Объект состояния - reactive
const state = reactive({
items: [
{ id: 1, name: "Apple" },
{ id: 2, name: "Banana" },
{ id: 3, name: "Orange" },
],
})
// Фильтрация - computed
const filteredItems = computed(() => {
const term = search.value.toLowerCase()
return state.items.filter(item =>
item.name.toLowerCase().includes(term),
)
})
function setSearch(value) {
// Здесь обновляем строку поиска
search.value = value
}
return {
search,
state,
filteredItems,
setSearch,
}
},
}
В этом примере вы видите связку:
- «сырые» данные — state;
- контролируемый ввод — search;
- производная коллекция — filteredItems.
Как фреймворки отслеживают зависимости
Чтобы вы лучше понимали, что происходит при работе с реактивными переменными, давайте очень коротко разберем механизм отслеживания зависимостей. Это поможет избежать странных багов.
Автоматическая регистрация зависимостей
Основная идея:
- Есть глобальное понятие «текущий эффект» — например, рендер компонента или вычисление computed.
- Когда этот эффект выполняется, он читает реактивные значения.
- Реактивная система запоминает, что «эффект A зависит от переменной X».
- Когда X меняется, система знает, какие эффекты нужно запустить снова.
Простейший псевдокод:
// Глобальная переменная с текущим эффектом
let currentEffect = null
function autorun(effect) {
// Здесь мы устанавливаем текущий эффект
currentEffect = effect
effect() // Выполняем функцию - в ней будут чтения реактивных переменных
currentEffect = null
}
function createReactive(initialValue) {
let value = initialValue
const deps = new Set() // Список эффектов, которые зависят от этой переменной
function get() {
if (currentEffect) {
// Если сейчас выполняется эффект - регистрируем зависимость
deps.add(currentEffect)
}
return value
}
function set(newValue) {
if (Object.is(newValue, value)) return
value = newValue
// Запускаем все эффекты, которые зависят от этой переменной
deps.forEach(effect => effect())
}
return { get, set }
}
// Здесь мы создаем реактивную переменную count
const count = createReactive(0)
// Регистрируем эффект с помощью autorun
autorun(() => {
console.log("Сейчас count =", count.get())
})
// Меняем значение - эффект будет вызван еще раз
count.set(1) // Выведет "Сейчас count = 1"
Реальные фреймворки используют более сложные структуры (WeakMap, Map, отдельные трекеры для полей объектов), но принцип остается таким же.
Реактивность в Svelte
Svelte предлагает одну из самых простых для восприятия форм реактивных переменных — вам даже не нужно вызывать специальные функции.
Базовая реактивность через присваивание
В Svelte любая переменная в компоненте считается «реактивной», если ее значение используется в шаблоне.
<script>
// Здесь мы создаем реактивные переменные count и double
let count = 0
let double = count * 2
function increment() {
// Простое присваивание - триггерит реактивность
count = count + 1
double = count * 2
}
</script>
<p>Счетчик - {count}</p>
<p>Удвоенное значение - {double}</p>
<button on:click={increment}>+1</button>
Svelte компилирует компонент в чистый JS-код, где:
- переменные превращаются в поля;
- шаблон — в набор функций обновления;
- каждое присваивание вызывает соответствующие функции обновления.
Реактивные метки ($:)
Чтобы не пересчитывать все вручную, есть «реактивные метки»:
<script>
// Здесь мы создаем реактивную переменную count
let count = 0
// Здесь мы задаем реактивное выражение для double
$: double = count * 2 // Вычислится при каждом изменении count
function increment() {
count++
}
</script>
<p>Счетчик - {count}</p>
<p>Удвоенное значение - {double}</p>
<button on:click={increment}>+1</button>
Как видите, это очень похоже на computed, только в более декларативной записи.
Реактивность в React - useState и useEffect
В React нет «классической» реактивности через Proxy или get/set, но на уровне поведения эффекты похожи: изменение состояния вызывает повторный рендер, а useEffect позволяет подписаться на изменения зависимостей.
useState как минимальная форма реактивной переменной
import { useState } from "react"
function Counter() {
// Здесь мы создаем "реактивную" пару [state, setState]
const [count, setCount] = useState(0)
function increment() {
// Обновление состояния триггерит повторный рендер компонента
setCount(prev => prev + 1)
}
return (
<div>
<p>Счетчик - {count}</p>
<button onClick={increment}>+1</button>
</div>
)
}
Для разработчика это напоминает работу с реактивной переменной: при изменении значения интерфейс обновляется. Но технически React просто вызывает компонент как функцию еще раз и генерирует новый «виртуальный DOM», а потом сравнивает его с предыдущим.
useEffect как подписка на изменения
useEffect — это аналог подписки на изменения зависимостей:
import { useState, useEffect } from "react"
function Logger() {
// Здесь мы создаем состояние count
const [count, setCount] = useState(0)
useEffect(() => {
// Этот эффект выполнится каждый раз, когда изменится count
console.log("Новое значение count:", count)
}, [count]) // Массив зависимостей
return (
<button onClick={() => setCount(c => c + 1)}>
Увеличить - {count}
</button>
)
}
По сути:
- count — состояние, изменение которого вызывает перерендер;
- useEffect с зависимостью [count] — функция, подписанная на это состояние.
Если посмотреть концептуально, это тоже реактивная модель, просто реализованная не через Proxy, а через явные вызовы setState и эффекты.
Типовые сценарии использования реактивных переменных
Теперь давайте посмотрим, какие типовые задачи удобно решать через реактивные переменные.
Управление формами
Формы — идеальный пример: значение поля меняется, и вы хотите автоматически:
- показывать ошибки валидации;
- изменять доступность кнопок;
- отображать подсказки.
На примере Vue:
import { reactive, computed } from "vue"
export default {
setup() {
// Здесь мы создаем реактивное состояние формы
const form = reactive({
email: "",
password: "",
rememberMe: false,
})
// Здесь мы создаем реактивные ошибки
const errors = reactive({
email: "",
password: "",
})
const isValid = computed(() => {
// Простая проверка - есть ли ошибки
return !errors.email && !errors.password
})
function validateEmail() {
// Проверяем email и записываем ошибку при необходимости
errors.email = form.email.includes("@") ? "" : "Некорректный email"
}
function validatePassword() {
// Простая проверка длины пароля
errors.password =
form.password.length >= 6 ? "" : "Минимальная длина 6 символов"
}
function submit() {
// Перед отправкой формы запускаем валидацию
validateEmail()
validatePassword()
if (!isValid.value) return
console.log("Отправляем данные", form)
}
return {
form,
errors,
isValid,
validateEmail,
validatePassword,
submit,
}
},
}
В шаблоне изменение form.email автоматически приводит к пересчету isValid и, например, дизейблу кнопки.
Фильтрация и сортировка списков
Еще один типичный пример — списки и таблицы:
import { reactive, ref, computed } from "vue"
export default {
setup() {
// Здесь мы создаем исходный список
const state = reactive({
items: [
{ id: 1, name: "John", age: 30 },
{ id: 2, name: "Anna", age: 25 },
{ id: 3, name: "Mike", age: 35 },
],
})
// Здесь мы создаем критерии фильтрации и сортировки
const search = ref("")
const sortBy = ref("name") // name или age
const sortDir = ref("asc") // asc или desc
const filteredAndSorted = computed(() => {
const term = search.value.toLowerCase()
let result = state.items.filter(item =>
item.name.toLowerCase().includes(term),
)
result = result.slice().sort((a, b) => {
const field = sortBy.value
if (a[field] < b[field]) return sortDir.value === "asc" ? -1 : 1
if (a[field] > b[field]) return sortDir.value === "asc" ? 1 : -1
return 0
})
return result
})
return {
state,
search,
sortBy,
sortDir,
filteredAndSorted,
}
},
}
Как только пользователь вводит текст или меняет режим сортировки, filteredAndSorted пересчитывается автоматически, и таблица обновляется без дополнительных вызовов.
Типичные ошибки и подводные камни
Реактивность упрощает жизнь, но вносит и свои нюансы. Здесь я покажу вам частые проблемы и как их избежать.
Потеря реактивности при деструктуризации
Во Vue, если вы деструктурируете reactive-объект, вы теряете реактивность полей:
import { reactive } from "vue"
const state = reactive({ count: 0 })
// Плохо - count больше не реактивен
const { count } = state
count++ // Vue не увидит изменение
Как решать:
- использовать toRefs или toRef;
- или использовать ref для отдельных значений изначально.
import { reactive, toRefs } from "vue"
const state = reactive({ count: 0 })
// Правильно - count остается реактивным
const { count } = toRefs(state)
// Теперь изменение count.value будет реактивным
count.value++
Комментарии в коде помогают здесь явно показать, что происходит с реактивностью.
Мутация исходных данных «в обход» реактивной системы
Иногда разработчики пытаются напрямую изменять вложенные объекты, которые не были сделаны реактивными.
const user = {
name: "Alex",
}
// Здесь мы просто присваиваем объекту user реактивное поле profile
state.user = user
// А потом меняем user.name напрямую
user.name = "John" // Vue не узнает об этом изменении
Лучший подход — изначально оборачивать данные в reactive или ref, а не хранить «сырые» объекты, которые вы потом мутируете где‑то еще.
Неочевидные зависимости в computed
Иногда вы случайно используете внутри computed не реактивную переменную, а внешнюю, и ожидаете, что computed будет обновляться. Но он не обновится, потому что зависимость не была зарегистрирована.
import { ref, computed } from "vue"
let external = 1 // Нереактивная переменная
export default {
setup() {
// Здесь мы создаем реактивную переменную count
const count = ref(0)
const total = computed(() => {
// Здесь external не реактивен - изменение external
// не вызовет пересчет total
return count.value + external
})
return {
count,
total,
}
},
}
Если вы хотите, чтобы external влиял на реактивную систему, его нужно сделать реактивным (ref или reactive).
Как реализовать свою маленькую реактивную систему
Чтобы лучше почувствовать концепцию, полезно однажды написать минималистичную реализацию. Давайте сделаем простой пример на JS.
Реактивные переменные и эффекты
// Глобальная структура зависимостей
// target -> key -> Set<effect>
const targetMap = new WeakMap()
let activeEffect = null
function effect(fn) {
// Здесь мы создаем обертку над функцией
const wrapper = () => {
activeEffect = wrapper // Устанавливаем текущий эффект
fn() // Выполняем функцию - она прочитает реактивные значения
activeEffect = null // Сбрасываем активный эффект
}
wrapper()
}
// Функция трекинга зависимостей
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect) // Регистрируем текущий эффект как зависимость
}
// Функция триггера зависимостей
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
dep.forEach(effect => effect()) // Вызываем все эффекты
}
// Реализация reactive через Proxy
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // Регистрируем зависимость
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (!Object.is(oldValue, value)) {
trigger(target, key) // Вызываем эффекты при изменении
}
return result
},
})
}
// Здесь мы создаем реактивный объект state
const state = reactive({ count: 0 })
// Регистрируем эффект
effect(() => {
console.log("Счетчик:", state.count)
})
// Меняем значение - эффект вызовется снова
state.count++ // Выведет "Счетчик: 1"
state.count++ // Выведет "Счетчик: 2"
Этот код, конечно, упрощен по сравнению с Vue или MobX, но он демонстрирует, как связаны:
- Proxy;
- глобальный стэк эффектов;
- таблица зависимостей.
Практические рекомендации по работе с реактивными переменными
Старайтесь держать состояние ближе к месту использования
Чем меньше «глобальных» реактивных переменных, тем проще отлаживать код. Реактивность удобнее всего, когда:
- состояние компонента живет в самом компоненте;
- общие данные выносятся в отдельный store (например, Pinia, Redux, MobX-stores);
- вы явно контролируете, какой компонент от чего зависит.
Разделяйте «сырые» данные и производные
Хорошая практика — думать о состоянии как о трех слоях:
- Базовые данные (то, что приходит с сервера, вводит пользователь).
- Локальные настройки (фильтры, режимы сортировки, переключатели).
- Производные значения (фильтрованные, отсортированные, агрегированные данные).
Для слоя 3 всегда используйте computed (или аналог в вашем фреймворке). Это:
- делает код более декларативным;
- уменьшает количество багов из серии «забыли где‑то обновить значение».
Осторожнее с побочными эффектами
Иногда хочется внутри computed или реактивного выражения сделать запрос на сервер или изменить что‑то еще в состоянии. Лучше этого избегать.
Рекомендация:
- computed и реактивные выражения — только про вычисления без побочных эффектов;
- побочные эффекты (запросы, логирование, таймеры) — через специальные механизмы (watch, useEffect, onMount и т.п.).
Заключение
Реактивные переменные — это фундаментальная концепция, вокруг которой строятся современные фреймворки. Изменение значения автоматически обновляет все, что от него зависит. За этим стоит механизм:
- отслеживания чтений реактивных данных;
- регистрации зависимостей (эффектов, компонентов, вычислений);
- повторного запуска этих эффектов при изменении значений.
Вы увидели, как это выглядит во Vue (ref, reactive, computed), как аналогичные идеи проявляются в Svelte и React, и какими паттернами удобно пользоваться в реальных задачах: формы, фильтрация, производные значения.
Понимание того, как устроена реактивность под капотом, помогает:
- писать более предсказуемый код;
- избегать скрытых зависимостей и «магии»;
- эффективнее использовать возможности фреймворка, а не бороться с ним.
Частозадаваемые технические вопросы по теме и ответы
Как отследить изменение реактивной переменной и выполнить побочный эффект (запрос, лог)?
Во Vue используйте watch:
import { ref, watch } from "vue"
const count = ref(0)
// Здесь мы подписываемся на изменения count
watch(count, (newValue, oldValue) => {
console.log("count изменился с", oldValue, "на", newValue)
// Здесь можно выполнить запрос или другой побочный эффект
})
В React аналог — useEffect с зависимостью:
useEffect(() => {
console.log("count изменился", count)
// Здесь выполняем побочный эффект
}, [count])
Как сделать реактивной вложенную структуру (объект в объекте) во Vue 3?
reactive оборачивает объект целиком, включая вложенные объекты. Но если вы заменяете вложенный объект целиком, делайте это через само реактивное поле:
const state = reactive({
user: { name: "Alex", profile: { city: "London" } },
})
// Правильно - меняем через state.user.profile
state.user.profile.city = "Paris"
// Если нужно заменить профиль целиком
state.user.profile = { city: "Berlin" } // Это тоже реактивно
Проблемы чаще возникают, когда вы храните вне реактивного состояния ссылку на вложенный объект и мутируете его напрямую. Старайтесь работать через исходный reactive-объект.
Как в Svelte отреагировать на изменение одной переменной и пересчитать другую без ручного вызова функции?
Используйте реактивные метки:
<script>
let count = 0
// Здесь мы задаем зависимость: double всегда равно count * 2
$: double = count * 2
// Если нужно выполнить побочный эффект:
$: if (count > 10) {
console.log("count больше 10")
}
</script>
Метка $: говорит компилятору: «пересчитывай этот фрагмент кода каждый раз, когда меняются использованные в нем переменные».
Как в React сделать вычисляемое значение, похожее на computed из Vue?
Самый простой способ — использовать useMemo:
import { useState, useMemo } from "react"
function Component() {
const [price, setPrice] = useState(100)
const [quantity, setQuantity] = useState(2)
// Здесь мы создаем "вычисляемое" значение total
const total = useMemo(() => {
return price * quantity
}, [price, quantity]) // Зависимости
// total будет пересчитываться только при изменении price или quantity
...
}
Важно не путать useMemo (кэширование вычислений) с useEffect (побочные эффекты).
Как отладить, какие реактивные переменные заставляют компонент перерендериваться?
Подход зависит от фреймворка:
- Vue — используйте Vue Devtools, вкладка Component / Pinia, где видны реактивные поля и события изменений.
- React — React DevTools, включите Highlight updates, чтобы видеть, какие компоненты перерисовываются.
- Svelte — Svelte Devtools, можно смотреть состояние и реактивные выражения.
На уровне кода полезно временно добавлять console.log в computed, эффекты и обработчики, чтобы понять, какие значения действительно меняются и что запускает обновление.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев