State
State<A> — реактивное значение: текст поля, текущий пользователь, счётчик. У него всегда есть значение, которое можно прочитать, к нему может привязаться JSX, из него выводятся другие значения. (В FRP-литературе этот тип называется Behavior — «значение во времени», — но для использования теория не нужна.)
State или Stream?
Если это можно нарисовать на экране — State. Если на это можно среагировать — Stream. Длинная версия — с градусником и стуком в дверь: Чем Stream отличается от Behavior.
Создание
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()); // читать мир при samplestate — повседневный способ завести состояние: ячейка со значением и методом .set(), как useState без перезапусков. Событийные формы (accum, hold) хороши, когда состояние по природе — «история случившегося»; о них — в Streams. На уровне модуля они обёрнуты в root(...): состояние живёт в скоупе-владельце, а у модуля своего скоупа нет (внутри компонента он есть автоматически).
Установка значения, равного текущему (по Object.is), — no-op: ни один подписчик не проснётся, DOM не тронется. State — это значение во времени, «поменять» его на то же самое — не изменение. Своё сравнение передаётся вторым аргументом (state(user, (a, b) => a.id === b.id)), а () => false доставляет каждый вызов. Обратная сторона: мутировать объект на месте и сеттить ту же ссылку больше не работает — кладите новый объект.
Для «прочитай и измени» есть update():
count.update((n) => n + 1);
todos.update((list) => [...list, item]);В отличие от set(count.sample() + 1), апдейтер фолдится над значением, застейдженным текущим моментом: несколько update внутри одного batch компонуются (+2, а не +1) — тогда как sample() внутри батча всё ещё показывает домоментное значение (это задокументированная задержка hold). Равный результат пропускается, как и любой set.
Переходы состояния
Когда одна ячейка сводит несколько потоков, объявляйте переходы прямо на ней:
const count = state(0)
.on(inc, (n) => n + 1)
.on(dec, (n) => n - 1)
.on(reset, () => 0);Редьюсер получает (состояние, событие); вхождение и обновление ячейки — один момент. .on регистрирует процесс в текущем скоупе — в компоненте он есть автоматически, на уровне модуля оберните определение в root(...).
Производные
Производные значения строятся через map и combine, и структура выражения — это и есть граф зависимостей:
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()возвращает текущее значение.
const submitted = draft.at(submits); // значение draft на момент сабмитаUpdates
b.updates — это Stream<A> изменений State, мост обратно в мир событий; им пользуются комбинаторы вроде debounce(query.updates, 300).
State обновляется на границе момента: внутри транзакции, которая его меняет, читатели видят старое значение. На один момент ячейка доставляет одно коалесцированное вхождение updates: несколько set в одном batch — подписчики видят только финальное значение. Именно эта задержка делает одновременные чтения определёнными — вся история в Транзакциях.
Незнакомый термин? Вся терминология этой документации объяснена в глоссарии.