signal, computed & effect
Точечная реактивность для состояния приложения. signal хранит значение; чтение .value внутри
effect, computed или UI-привязки подписывает её, а запись .value заново выполняет ровно
этих подписчиков. Никакого virtual DOM и никакого ре-рендера: запись сигнала ложится точечным
обновлением свойства на удержанном дереве UI.
Обзор
const count = signal(0)
const label = computed(() => `Count: ${count.value}`)
const screen = UIScreen([
UIColumn([
UIText(() => label.value) // привязка текста — обновляется сама
.style({ color: "white", fontSize: 20 }),
UIButton([ UIText("+1").style({ color: "white" }) ])
.style({
height: 40, borderRadius: 8, justifyContent: "center",
bgColor: () => (count.value > 9 ? "#22c55e" : "#FF4032"), // привязка стиля
})
.onClick(() => count.value++),
]).style({ gap: 8, p: 16 }),
])
Router.push(screen)Нигде нет ручного label.text = … — привязки перевыполняются, когда меняется count.
API
const s = signal(0) // Signal<number>
s.value // чтение (подписывает внутри effect/computed/привязки)
s.value = 1 // запись — уведомляет подписчиков, пакетно за микрозадачу
s.peek() // чтение БЕЗ подписки
const c = computed(() => s.value * 2) // Computed<number> — ленивый, кэшируется до смены зависимости
c.value
const dispose = effect(() => { // выполняется сейчас и снова после смены любого прочитанного сигнала
console.log("count is", s.value)
})
dispose() // остановить; неостановленные effect живут весь запуск приложения- Записи пакетируются: несколько записей
.value =за один тик дают одно перевыполнение на каждый затронутый effect (на следующей микрозадаче). Запись равного значения (Object.is) — no-op. - Зависимости отслеживаются на каждый прогон: ветка, переставшая читать сигнал, отписывается от него.
effectвозвращает свою функцию остановки. UI-привязкам остановка не нужна — они живут и умирают вместе со своим элементом.
UI-привязки
Везде, где UI-элемент принимает текстовую строку или примитивное значение стиля, функция
() => value вместо этого создаёт привязку:
UIText(() => `${items.value.length} items`) // реактивный текст
el.style({ opacity: () => (open.value ? 1 : 0) }) // реактивное свойство стиля (форма слияния)
el.style.transform = () => `translateY(${y.value}px)` // реактивная запись одного свойстваПравила:
- Привязки выполняются один раз синхронно при создании, поэтому элемент сразу держит конкретное значение.
- Последующий
.style({ prop: staticValue })заменяет привязку по этому ключу; последующий.style({ prop: () => … })перепривязывает её. (Прямая записьel.style.prop = staticValueне снимает привязку — следующая смена сигнала перезапишет её.) - Только верхний уровень: функции-значения внутри вложенных блоков состояний (
onPressed: { … },onLandscape: { … }) — не привязки. - Ручная установка
.textна привязанномUITextработает, но перезаписывается при следующей смене сигнала.
Реактивные дети
Контейнер также принимает функцию в позиции детей — список сам себя перерисовывает, когда меняется прочитанный им сигнал:
const todos = signal([ { title: "one", done: false } ])
UIColumn(() => todos.value.map(todo =>
UIRow([ UIText(todo.title) ])
))
// условный рендер — ложные записи просто исчезают:
UIColumn(() => [ header, expanded.value && details ])
todos.value = [ ...todos.value, { title: "two", done: false } ] // вставлен один узел, остальные не тронутыОбновления минимальны: рантайм сравнивает по идентичности элементов — ушедшие узлы удаляются,
новые вставляются на свою позицию, неизменные остаются смонтированными. Обычный .map(…) внутри
функции-детей мемоизируется покомпонентно компилятором (через внутренний хелпер __uiMap — сам его
никогда не зови), поэтому объект-элемент, оставшийся в списке, сохраняет свой элемент между
обновлениями. Отсюда правила:
- Идентичность элемента — это ссылка на объект. Обновляй списки, записывая новый массив,
переиспользующий неизменные объекты-элементы (
[ ...items.value, added ],.filter(…)). Пересоздание каждого объекта-элемента при каждой записи (items.value.map(i => ({ ...i }))) вызывает полную перерисовку. - Колбэк рендера выполняется один раз на новый элемент (как компонент), а не на каждое изменение — не клади туда побочные эффекты в расчёте на прогон при каждом обновлении.
- Контейнер с функцией-детьми принадлежит этой привязке — не вызывай на нём ещё и
.append()/.remove()вручную. - Переставленный элемент удаляется и вставляется заново (его состояние — фокус ввода, скролл — сбрасывается).
- Для сотен строк используй
UIVirtualizedList; функции-дети — для маленьких и средних списков.
el.setContent(() => …) привязывается так же.
Реактивные классы: bindClass
Привяжи $-класс-стиля к сигналу — переходы, объявленные на классе, применяются автоматически:
row.style({ $done: { opacity: 0.4, duration: 150 } })
.bindClass("done", () => todo.done.value)Автоматическая обёртка (что компилятор делает за тебя)
В коде проекта текстовый аргумент UIText или значение стиля верхнего уровня, читающее .value
сигнала, оборачивается в привязку на этапе компиляции — поэтому явная форма () => в этих позициях
необязательна:
UIText(`Count: ${count.value}`) // компилируется в UIText(() => `…`)
btn.style({ bgColor: active.value ? "#f43" : "#333" }) // компилируется в привязку стиляФормы с () => остаются валидными и ведут себя идентично. Чтения внутри твоих собственных
функций-хелперов (UIText(fmt())) не распознаются — там используй явную стрелку.
Подводные камни
// ✗ сначала прочитать .value в обычную переменную — чтение происходит один раз, дальше ничего не обновляется
const n = count.value
UIText(`Count: ${n}`)
// ✓ читай .value прямо в текстовом выражении (авто-обёртка) или пиши привязку явно
UIText(`Count: ${count.value}`)
UIText(() => `Count: ${count.value}`)
// ✗ сигнал с массивом, изменённым на месте — та же ссылка, уведомления нет
items.value.push(x)
// ✓ запиши новую ссылку
items.value = [ ...items.value, x ]
// ✗ ждать, что DOM изменится синхронно после записи (обновления сбрасываются на микрозадаче)
count.value = 5; /* label ещё не перерисован на этой строке */Смотрите также
- Стилизация UI — свойства стиля, на которые могут нацеливаться привязки.
- Элементы контента —
UIText. - animate — твины по времени (сигналы — про состояние, не про время).