Skip to content

State

State<A>реактивное значение: текст поля, текущий пользователь, счётчик. У него всегда есть значение, которое можно прочитать, к нему может привязаться JSX, из него выводятся другие значения. (В FRP-литературе этот тип называется Behavior — «значение во времени», — но для использования теория не нужна.)

State или Stream?

Если это можно нарисовать на экране — State. Если на это можно среагироватьStream. Длинная версия — с градусником и стуком в дверь: Чем Stream отличается от Behavior.

Создание

ts
import { state, constant, State, root } from "@continuum-js/frp";

const name = state(""); // ячейка: name.sample() читает, name.set() пишет
const count = root(() => clicks.accum(0, (_e, n) => n + 1)); // свёртка события
const latest = root(() => responses.hold(null)); // последнее вхождение
const pi = constant(3.14159); // не меняется никогда
const now = State.fromPoll(() => Date.now()); // читать мир при sample

state — повседневный способ завести состояние: ячейка со значением и методом .set(), как useState без перезапусков. Событийные формы (accum, hold) хороши, когда состояние по природе — «история случившегося»; о них — в Streams. На уровне модуля они обёрнуты в root(...): состояние живёт в скоупе-владельце, а у модуля своего скоупа нет (внутри компонента он есть автоматически).

Установка значения, равного текущему (по Object.is), — no-op: ни один подписчик не проснётся, DOM не тронется. State — это значение во времени, «поменять» его на то же самое — не изменение. Своё сравнение передаётся вторым аргументом (state(user, (a, b) => a.id === b.id)), а () => false доставляет каждый вызов. Обратная сторона: мутировать объект на месте и сеттить ту же ссылку больше не работает — кладите новый объект.

Для «прочитай и измени» есть update():

ts
count.update((n) => n + 1);
todos.update((list) => [...list, item]);

В отличие от set(count.sample() + 1), апдейтер фолдится над значением, застейдженным текущим моментом: несколько update внутри одного batch компонуются (+2, а не +1) — тогда как sample() внутри батча всё ещё показывает домоментное значение (это задокументированная задержка hold). Равный результат пропускается, как и любой set.

Переходы состояния

Когда одна ячейка сводит несколько потоков, объявляйте переходы прямо на ней:

ts
const count = state(0)
  .on(inc, (n) => n + 1)
  .on(dec, (n) => n - 1)
  .on(reset, () => 0);

Редьюсер получает (состояние, событие); вхождение и обновление ячейки — один момент. .on регистрирует процесс в текущем скоупе — в компоненте он есть автоматически, на уровне модуля оберните определение в root(...).

Производные

Производные значения строятся через map и combine, и структура выражения — это и есть граф зависимостей:

ts
const total = items.map((xs) => xs.reduce((s, i) => s + i.price, 0));
const label = combine(total, currency, (t, c) => `${t} ${c}`);

Если значение вычислимо из существующих States — не храните его, выводите. combine комбинирует несколько источников (два, три и больше); вывод свободен от глитчей (см. Транзакции).

Чистые производные — формулы: map/combine/filter — просто рецепты, пока их никто не слушает (привязка в JSX, .listen() или узел ниже, у которого есть свои слушатели). Первый слушатель подключает всю цепочку вверх до источников; после ухода последнего цепочка засыпает. Спящая формула не стоит ничего, а sample() отвечает верно всегда — и тёплой, и спящей, пересчитав по рецепту. Стейтовые производные (hold, accum) — не формулы, а процессы: их значение зависит от каждого вхождения, поэтому они живут вместе со скоупом-владельцем. Внутри компонента владелец есть автоматически; на уровне модуля оберните определение в root(...) — иначе рантайм остановит вас обучающей ошибкой.

Чтение

  • В JSX — кладите сам State: {count}, class={cls}. Это привязка, а не снимок.
  • Из событияb.at(e); у него точная семантика одновременности.
  • Вне сети (инициализация, тесты, интеграции) — b.sample() возвращает текущее значение.
ts
const submitted = draft.at(submits); // значение draft на момент сабмита

Updates

b.updates — это Stream<A> изменений State, мост обратно в мир событий; им пользуются комбинаторы вроде debounce(query.updates, 300).

State обновляется на границе момента: внутри транзакции, которая его меняет, читатели видят старое значение. На один момент ячейка доставляет одно коалесцированное вхождение updates: несколько set в одном batch — подписчики видят только финальное значение. Именно эта задержка делает одновременные чтения определёнными — вся история в Транзакциях.


Незнакомый термин? Вся терминология этой документации объяснена в глоссарии.