- PVSM.RU - https://www.pvsm.ru -
Привет, меня зовут Артём Арутюнян и я автор менеджера состояния Reatom. Этим постом открывается серия обучающих материалов на русском языке, документация на английском доступна на официальном сайте [1].
А оно вам надо? Думаю, да, потому что Reatom — это универсальное решение, которое позволяет легко пошарить глобальное состояние за микроскопическую (2.5KB [2]) цену, эффективно строить самодостаточные и переиспользуемые логические модули гигантских приложений или просто сделать ваш сетевой кеш реактивным с помощью дополнительного пакета @reatom/async [3].
В этой статье мы кратко пройдёмся по мотивации и истории, а потом разберём основные фичи и примеры их использования вместе с биндингами к React.js. Похожий разбор есть в виде скринкаста [4].
Я ленивый идеалист с каким-то пунктиком на перформанс (у меня часто были маломощные ноутбуки и смартфоны) и я всё время попадал на проекты с сотнями тысяч или миллионом строк кода, хотя иногда касался и мелких стартапов. И всегда мне хотелось использовать одно решение, не переключаясь между контекстами, которое будет и маленькое, и эффективное, и позволяло бы использовать энтерпрайзнутые паттерны, которые нужны, когда цена любого рефакторинга может быть огромной.
Мне нравилось, как реактивность решает проблемы связанности кода, а иммутабелность упрощает дебаг — это и стало главными столпами разрабатываемой библиотеки.
Сложно сделать хорошо всё и сразу, поэтому эволюция Reatom заняла годы.
Первый релиз был осенью 2019-го, хотя ему предшествовали почти два года исследований. Началось всё в феврале 2018-го, тогда меня передёрнуло от function-tree [5], и я решил сделать с подобным апи убийцу редакса (тогда это было популярным занятием). Далее история долгая: погружение в дзен вывода типов TypeScript, десятки прототипов, постоянные попытки выжать лучшее из современных технологий. Исследование алгоритмов обхода графов для решения проблемы глитчей [6]. Рост комьюнити и попытки писать понятную документацию. Обслуживание инфраструктуры монорепы. Погружение в теорию баз данных, которые я администрировал ещё в 2014-м, но не задавался вопросами подкапотной архитектуры. Переосмысление архитектуры веб-приложений и состояния как явления. Один из артефактов всего этого — недавняя статья «Что такое состояние» [7], в которой изложены ключевые принципы архитектуры менеджера состояния.
Но главное — первая LTS и вторая версия реатома пытались быть совместимы с редаксом, и сколько я ни старался, нормально это сделать не выходило, он просто фундаментально сломан:
Бойлерплейт для меня всегда был меньшей проблемой, но вы просто посмотрите [9] на эту разницу между тулкитом(!) и реатомом. По ссылке используется пакет @reatom/framework, который включает в себя базовый набор самых часто используемых пакетов и просто реэкспортит из них всё для удобства установки и импорта. В последние пару лет требования к развитой экосистеме всё важнее. В 2020-м было нормально иметь маленькую библиотеку и дать на откуп пользователей писать и публиковать в NPM свои хелперы. Но сейчас индустрия уже повзрослела и предъявляет взвешанные требования к экосистеме, её слаженности и поддержке. Все пакеты Reatom хранятся в монорепе, что позволяет тестировать любое изменение со всеми зависимостями и синхронизировать релизный цикл, сделав его предсказуемым.
Это что касается технического аспекта поддержки, в общем же политика выглядит так: нечётные релизы считаются LTS и поддерживаются несколько лет. Первая версия поддерживалась три года, сейчас можно подменить импорты и использовать код на ней дальше с новыми фичами и дальнейшей поддержкой. Текущая третья версия (@reatom/core@3.x.x) будет актуальна ещё несколько лет. Раз в год возможны небольшие ломающие изменения от рефакторинга типов.
Cперва взглянем на код базового примера из этой песочницы [10]:
import { action, atom } from '@reatom/core'
import { useAction, useAtom } from '@reatom/npm-react'
const inputAtom = atom('')
const greetingAtom = atom((ctx) => `Hello, ${ctx.spy(inputAtom)}!`)
const onChange = action((ctx, event) =>
inputAtom(ctx, event.currentTarget.value),
)
export const Greeting = () => {
const [input] = useAtom(inputAtom)
const [greeting] = useAtom(greetingAtom)
const handleChange = useAction(onChange)
return (
<>
<input value={input} onChange={handleChange} />
{greeting}
</>
)
}
Это самый базовый пример трёх ключевых сущностей: контекст, атом, экшен. На их основе можно реализовать большинство популярных паттернов из ФП или ООП и расширять по необходимости дополнительными фичами, больше десятка существующих пакетов этому пример. Но давайте разберём каждую строчку детальней.
Конечно, больше всего вопросов вызывает ctx
. Это некий глобальный DI-контейнер на стероидах, заточенный под стейт-менеджемент. Он позволяет читать актуальный стейт атома ctx.get(anAtom)
, подписываться на него ctx.subscribe(anAtom, newState => sideEffect(newState))
и планировать сайд-эффекты во время транзакции, но об этом попозже. Главное, что нужно запомнить — ctx
прокидывается первым аргументом в большинстве колбэков реатома и каждый раз приходит новый (под капотом содержит весь стек предыдущих контекстов вплоть до глобального).
inputAtom
— базовый атом и кирпичик, с которого всё начинается. Это переменная для реактивного доступа и обновления данных. Хорошей практикой является разделять все ваши состояния на множество атомов с примитивными значениями. Для пользователей редакса это может быть чуждо, но в процессе использования всё больше будет ощущаться профит от такого подхода.
Базовый атом может быть вызван как функция с новым значением или редьюсером этого значения: countAtom(ctx, 1)
и countAtom(ctx, state => state + 1)
. Такой вызов функции возвращает новое значение атома. В типах такой атом называется AtomMut
(mutable atom).
Благодаря такому апи код быстро и удобно парсить глазами:
ctx.get
иctx.spy
получают значение атома, аdoSome(ctx)
иsomeAtom(ctx, value)
мутируют.
greetingAtom
— вычисляемый атом, который вызывает переданную функцию при первой подписке и с помощью метода spy
в контексте подписывается на переданный атом и получает его значение. Это map
и combine
в одном флаконе, только гибче и удобнее. В типах такой атом называется просто Atom
. У вычисляемых значений реатома есть две киллер-фичи, каждая из которых отдельно встречается в ФП (редакс) или ООП (мобыкс) мире, но я ещё не встречал их вместе.
Первое — если вычисление в редьюсере (да, вторым аргументом приходит предыдущий стейт) упадёт с ошибкой, все предыдущие изменения в текущей транзакции откатятся и будет соблюдена атомарность (рассказывал об этом здесь [7]). Это важный аспект, позволяющий не допустить неконсистентные данные, и он важен для крупных приложений. Базовое поведение React практически такое же, даже жёстче, всё приложение размонтируется (в случае отсутствия componentDidCatch
).
Второе — вы можете использовать spy
в любом порядке (привет, правила хуков реакта). Вы можете применять его в условии и подписываться только на нужные атомы, когда это действительно актуально, что оптимизирует автоматически ваши потоки данных и помогает избавится от лишних вычислений.
const listAtom = atom([])
const listAggregatedAtom = atom(ctx => aggregate(ctx.spy(listAtom)))
const listViewAtom = atom((ctx) => {
if (ctx.spy(isAdminAtom)) {
return ctx.spy(listAggregatedAtom)
} else {
return ctx.spy(listAtom)
}
})
Подобный код на реселекте или ФРП-библиотеке, скорее всего, получал бы вместе isAdmin
, list
и listAggregated
и способствовал избыточным вычислениям (для isAdmin === false
). Конечно, в теории можно описать селекторы, которые будут делать примерно то же самое, но на практике так не заморачиваются и получают очередную каплю в замедление приложения. В реатоме такие условные подписки — базовый принцип.
Удобно использовать в вычисляемом атоме и простой ctx.get
для чтения какого-то значения — это не создаёт подписку, но гарантированно отдаёт самый актуальный стейт переданного атома.
На самом деле атомы не хранят значения, а являются лишь объектом с метаданными и ключом WeakMap-контекста, где и хранятся все стейты и связи между атомами. Это позволяет прозрачно и безопасно инстанцировать цепочки вычислений и упрощает SSR и тестирование [11].
onChange
— экшен, хелпер для батчинга изменений. Если у вас есть несколько атомов для последовательного обновления, каждое изменение будет тригерить их зависимые вычисления и подписчиков. Что бы забатчить вычисления, можно использовать колбэк в ctx.get(() => {...})
или просто создать выделенный экшен и произвести все апдейты в нём. Экшены удобны тем что им можно, как и атомам, давать имена (второй аргумент), что в дальнейшем упрощает дебаг @reatom/lgger. [12]
Про TypeScript, реатом разрабатывается с большим фокусом на автоматическом выводе типов и всегда старается понять переданные данные. Если вам необходимо затипизировать параметры экшена, просто укажите их тип у них же: action((ctx, event React.ChangeEvent) => ...). Больше рекомендаций по описанию типов ищите в документации [13].
Под капотом экшен — это атом со временным стейтом, хранящий params вызова и возвращённый payload. С ним можно делать всё то же, что и с атомом: подписываться через
ctx.subscribe
для сайд-эффектов иctx.spy
в вычисляемом атоме. Например, можно в вычисляемом атоме получить данные другого атома только при срабатывании какого-то экшена — это редкий, но очень удобный способ оптимизации. Больше примеров и возможных паттернов разберём в следующих статьях.
Стоит упомянуть о ctx.schedule
, который позволяет планировать сайд-эффекты, как useEffect
в реакте. Его можно вызывать где угодно, но чаще всего это пригождается в экшенах.
const onSubmit = action((ctx) => {
const input = ctx.get(inputAtom)
inputAtom(ctx, '')
ctx.schedule(() => api.submit({ input }))
})
Переданный в ctx.schedule
колбэк будет вызван после всех чистых вычислений, но до вызова подписчиков — это удобно, т. к. иногда эффекты просто сохраняют что-то в localStorage или делают другие не чистые, но синхронные операции и вызывают ещё апдейты. Подробности есть в документации [14], в общем же реатом старается всегда максимально отложить вызов подписчиков, чтобы избежать лишних ререндеров и предоставить самый последний и актуальный стейт. У редакса с этим ситуация радикально хуже.
Все пакеты-адаптеры имеют префикс платформы (
npm
,web
,node
), подробнее об этом можно почитать в документации [15].
Думаю, использование useAtom
и useAction
понятно и практически не нуждается в комментариях :) Хотя несколько вещей всё же нужно учесть.
В документации [16] к npm-react описаны обязательные инструкции по подключению реатома в провайдер реакта и настройке батчинга [17] для старой (<18) версии реакта.
import { createCtx } from '@reatom/core'
import { reatomContext } from '@reatom/npm-react'
const ctx = createCtx()
export const App = () => (
<reatomContext.Provider value={ctx}>
<Main />
</reatomContext.Provider>
)
Почему useAtom
возвращает кортеж, как useState
? Потому что его можно использовать как useState
! Вторым значением приходит колбэк обновления, который принимает новое значение или редьюсер.
export const Greeting = () => {
const [input, setInput] = useAtom(inputAtom)
const [greeting] = useAtom(greetingAtom)
return (
<>
<input value={input} onChange={e => setInput(e.currentTarget.value)} />
{greeting}
</>
)
}
Конечно, это будет работать только для AtomMut
— невычисляемого атома с примитивным начальным значением.
Но не будем на этом останавливаться. Вы можете не создавать отдельный атом и использовать его в разных местах через импорты, а можете использовать примитивное значение в useAtom
, и атом под капотом будет создан автоматически, а ссылку на него можно получить в третьем элементе кортежа. Также вы можете передать и вычисляемый редьюсер в useAtom
, как и в обычный atom
, и описать какой-то селектор прямо в компоненте.
export const Greeting = () => {
const [input, setInput, inputAtom] = useAtom('')
const [greeting] = useAtom((ctx) => `Hello, ${ctx.spy(inputAtom)}!`, [inputAtom])
return (
<>
<input value={input} onChange={e => setInput(e.currentTarget.value)} />
{greeting}
</>
)
}
Это может быть удобно для некоторых оптимизаций, смысл которых в передаче получившегося атома по ссылке в пропсы нижележащим компонентам. Подробнее этот паттерн мы разберём в следующих статьях.
Это всё, чем хотелось поделиться в первой статье, но реатом таит в себе ещё множество фич и архитектурных практик, которые сильно помогают в написании более чистого и тестируемого кода. Но обо всём поэтапно, ждите серию постов.
Можно лишь отметить, что любителям ФРП стоит обратить внимание на пакет @reatom/lens [18], а сторонникам более классической архитектуры — взглянуть на пакет @reatom/hooks [19], который позволяет писать более изолированный код, приближенный к акторам.
Ах да, и про реактивный кеш. Пакет @reatom/async [3] в связке с базовыми фичами реатома даёт большую часть фич react-query, а какие-то даже превосходит, всего за 3.4KB (gzip) [20].
Смотрите больше примеров на соответствующей странице [21] документации, добавляйтесь в Телеграм-канал [22] и чат [23]. И, конечно, оставляйте ваши комментарии и вопросы ниже.
Автор: Артём Арутюнян
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/381770
Ссылки в тексте:
[1] официальном сайте: https://www.reatom.dev
[2] 2.5KB: https://bundlejs.com/?q=%40reatom%2Fcore
[3] @reatom/async: https://www.reatom.dev/packages/async
[4] скринкаста: https://youtu.be/VqCV_J15_5o
[5] function-tree: https://www.npmjs.com/package/function-tree
[6] проблемы глитчей: https://en.wikipedia.org/wiki/Reactive_programming#Glitches
[7] «Что такое состояние»: https://habr.com/ru/company/ruvds/blog/706086/
[8] нет атомарности: https://github.com/artalar/state-management-specification/blob/master/src/index.test.js#L165
[9] посмотрите: https://github.com/artalar/RTK-entities-basic-example/pull/1/files#diff-43162f68100a9b5eb2e58684c7b9a5dc7b004ba28fd8a4eb6461402ec3a3a6c6
[10] песочницы: https://replit.com/@artalar/reatom-react-ts#src/App.tsx
[11] тестирование: https://www.reatom.dev/packages/testing
[12] @reatom/lgger.: https://www.reatom.dev/packages/logger
[13] документации: https://www.reatom.dev/guides/typescript
[14] документации: https://www.reatom.dev/core#ctxschedule
[15] документации: https://www.reatom.dev/guides/naming
[16] документации: https://www.reatom.dev/packages/npm-react
[17] настройке батчинга: https://www.reatom.dev/packages/npm-react#setup-batching-for-old-react
[18] @reatom/lens: http://reatom.dev/packages/lens
[19] @reatom/hooks: https://www.reatom.dev/packages/hooks
[20] 3.4KB (gzip): https://bundlejs.com/?q=%40reatom%2Fcore%2C%40reatom%2Fasync
[21] странице: https://www.reatom.dev/examples
[22] Телеграм-канал: https://t.me/reatom_ru_news
[23] чат: https://t.me/reatom_ru
[24] Источник: https://habr.com/ru/post/708826/?utm_source=habrahabr&utm_medium=rss&utm_campaign=708826
Нажмите здесь для печати.