Что такое FRP — на пальцах
Continuum построен на «функциональном реактивном программировании» (FRP). Звучит страшно. На деле вы им уже пользовались — если хоть раз открывали Excel. Эта страница объясняет всю идею без единого умного слова.
Вы уже это умеете: Excel
Впишите в ячейку C1 формулу =A1+B1. Теперь поменяйте A1 — и C1 обновилась сама. Вы не писали «когда меняется A1, пересчитай C1 и не забудь про D4, который зависит от C1». Таблица сама знает, что от чего зависит, и сама держит всё в актуальном состоянии.
Вот это и есть реактивное программирование. Всё. Ядро идеи — одна фраза:
Вы объявляете, что от чего зависит, а система сама следит, чтобы это всегда было правдой.
А как без этого живёт обычный код
В обычном JavaScript переменные — мёртвые ячейки. Поменяли одну — остальные об этом не узнают:
let a = 1;
let c = a + 1; // c = 2
a = 5; // а c так и осталась 2. Никто ей не сообщил.Поэтому весь классический UI-код — это ручная рассылка новостей: «кнопку нажали → обнови счётчик → и текст → и не забудь кнопку „Отправить" задизейблить». Каждое «не забудь» — будущий баг: забыли одно место — и страница показывает враньё.
React решает это по-своему: «не будем ничего рассылать — просто перерисуем весь компонент заново и сравним, что изменилось». Работает, но приходится бороться с последствиями перерисовок: мемоизация, массивы зависимостей, устаревшие замыкания.
FRP решает как Excel: не перерисовывать, а связать.
Три слова, которые нужно знать
Реактивное значение (в коде — State; в FRP-литературе такой тип называют Behavior). Это ячейка Excel: у неё всегда есть текущее значение, и все, кто от неё зависит, обновляются сами. Текст поля ввода, счётчик, текущий пользователь.
const count = state(0); // ячейка со значением 0Производное значение — формула. Не хранится, а вычисляется из других:
const double = count.map((n) => n * 2); // =count*2, всегда актуальноСобытие (в коде — Stream) — то, что случается, а не есть. Клик, нажатие клавиши, ответ сервера. У клика нет «текущего значения» — он либо произошёл, либо нет. Для новичка достаточно обычных колбэков (onClick={() => …}); события пригодятся позже, для потоков вроде debounce-поиска.
Чем Stream отличается от State — совсем на пальцах
Сравните две вещи из обычной жизни:
- Температура за окном. Она есть всегда. В любую секунду можно посмотреть на градусник и получить ответ. Вопрос «какая сейчас температура?» всегда имеет смысл.
- Стук в дверь. Он случается. Между стуками никакого стука нет — вопрос «какой сейчас стук?» бессмысленный. Зато осмысленны вопросы «постучали?» и «что делать, когда постучат?».
Температура — это State. Стук — это Stream. Вся разница — в том, какой вопрос вы задаёте:
| Вопрос | Тип | Примеры |
|---|---|---|
| «Какое оно сейчас?» | State | текст в поле, счётчик, выбранная вкладка, «залогинен ли», ширина окна |
| «Произошло ли? Что делать, когда произойдёт?» | Stream | клик, нажатие Enter, пришёл ответ сервера, тикнул таймер |
Проверка одной фразой: если это можно нарисовать на экране — это State. Если на это можно среагировать — это Stream. Число на кнопке рисуют — State. Сам щелчок не нарисуешь — на него реагируют — Stream.
Почему не обойтись одним типом? Попробуйте засунуть клик в «ячейку»: какое значение в ней между кликами? Придётся выдумывать флажок clicked = true и не забывать сбрасывать его обратно — классический источник багов «кнопка сработала дважды». А попробуйте наоборот сделать температуру событием — и чтобы узнать «сколько сейчас», придётся вспоминать последнее уведомление, а если его ещё не было — у вас вообще нет ответа. Каждая вещь неудобна в чужой шкуре — поэтому типа два.
Они дружат и превращаются друг в друга: «последний пришедший ответ сервера» — это уже значение (responses.hold(null): событие → State), а «моменты, когда счётчик изменился» — это уже событие (count.updates: State → событие).
При чём тут «функциональное»
Формулы в Excel — чистые: =A1+B1 не может тайком запустить телевизор или подменить соседнюю ячейку. Именно поэтому таблице можно верить.
«Функциональное» в FRP — то же обещание: производные значения описываются чистыми функциями (map, как формула), и повлиять на значение можно только через объявленные связи. Никаких «подкрутить сбоку». Отсюда главная гарантия: страница не может показать состояние, которого не было.
В Excel это тоже видно: меняя A1, вы никогда не увидите таблицу, пересчитанную «наполовину» — где C1 уже новая, а зависящая от неё D4 ещё старая. Continuum даёт ту же гарантию для интерфейса: любое изменение применяется целиком, одним неделимым шагом. (Внутри это называется «транзакция», но пользоваться этим словом не обязательно — оно просто означает «как в Excel».)
Последний кусочек: компонент — это сборка, а не рисунок
В React компонент — художник: состояние поменялось — перерисовал картину заново. В Continuum компонент — сборщик: он один раз собирает кусок страницы из ячеек и связей — и уходит. Дальше конструкция живёт сама: значение изменилось → обновился ровно тот кусочек DOM, который к нему привязан (мы называем это точечными обновлениями DOM).
function Counter() {
const count = state(0); // ячейка
return (
<button onClick={() => count.update((n) => n + 1)}>
count: {count} {/* этот текст привязан к ячейке навсегда */}
</button>
);
}{count} в JSX — не «вставить текущее число», а «этот текст теперь показывает count. Всегда». Функция Counter больше не выполнится ни разу — ей незачем.
Шпаргалка соответствий
| Excel | Continuum | По-русски |
|---|---|---|
| ячейка со значением | state(0) | реактивное значение |
формула =A1*2 | count.map(n => n * 2) | производное значение |
| формула от двух ячеек | combine(a, b, f) | производное от нескольких |
| пересчёт всей таблицы разом | транзакция | изменение применяется целиком |
| — (в Excel нет) | Stream | поток происшествий: клики, ответы |
Что дальше
- Быстрый старт — пощупать за пять минут.
- Понятия — по одной идее на страницу, тем же простым языком.
- Стало интересно, как это устроено внутри — Мышление в States и Streams.
Незнакомый термин? Вся терминология этой документации объяснена в глоссарии.