Краткая характеристика:
- У него много сторов и сторы могут зависеть друг от друга, а не один большой стор и селекторы. То есть он ближе к Эфектору, чем в Редаксу/MobX. Всё ради tree shaking.
- Он ближе к стору прямых измений. В публичном API нет экшенов. Но всё-таки value = 1 на манер MobX запрещены — значения можно менять только через спец. методы. И в синхронизации состояния с сервером экшены есть (просто скрыты из публичного API).
Плюсы:
- Может работать без Логакса, чисто как стейт-менеджер.
- API специально создан, чтобы хранить в сторах бизнес-логику, чем разгружать компоненты и упрощать переносимость приложения между фреймворками.
- От 157 байт (!) в вашем JS-бандле.
- Расчитан на агрессивный tree shaking, чтобы в JS-бандле был только код того состояния, которые используются в текущих страницах.
- Очень ленивый — сторы на которых никто не подписан выгружаются из памяти, а их бизнес-логика останавливается.
- Стейт-менеджер написан так, чтобы прятать сложную логику работы с сервером. Особенно когда нужен CRDT, разрешение конфликтов редактирования, синхронизация от веб-сокету. Сразу содержит в себе опциональные умные сторы работы через Логакс.
Базовыне абстракции стора:
-
Идея Logux State, что сторы — умные, они сами подписываются за событиями системы, содержат логику и валидации. Например, стор роутера сам следит за
window.onpopstate
и за кликами по ссылкам. Идея вынести максимум логики из Реакт-компонентов в сторы, так как сторы легче тестировать и переносить между UI-фреймворками. -
Особенность Logux State, то у сторов есть два состояния — остановлен и запущен (когда стор установил всех подписчиков). Сторы — ленивые. Когда появляется первый подписчик на изменения стора, он запускается (сам подписывается на события системы). Когда отписывается последний подписчик — стор останавливается.
-
У стора есть значение. Он значение появляется только когда стор запущен. Поэтому единственный способ прочитать значение — либо подписаться
value = useStore(store)
/store.listen(value => {})
, либо вызватьgetValue(store)
. -
Стор создаётся через:
export type StoreValue = number; export const store = createStore<StoreValue>(() => { // Код запуска return () => { // Код остановки } })
-
Менять всё значение стора можно через
store.set(newValue)
. Но если вызвать метод на остановленом сторе, то новое значение будет проигнорировано. -
Какие-то операции над сторами надо выносить в функции, а не методы сторы.
doSomthing(store)
, но НЕstore.doSomething()
. Всё ради tree-shaking. -
Но некоторые сторы могут что-то добавлять в объект стора
store.x
.
Расширяем абстракции стора:
-
Есть Map Store. Создаётся через
createMap
. У него значением может быть только объект. Появляется методstore.setKey(key, value)
.store.listen((value, changedKey) => {})
принимает ключ, который изменился. -
У Map Store значение — ссылка всегда на один и тот же объект. Вызов
setKey
илиset
меняет ключи в этом объекте, не меняя сам объект. -
Store Builder — функцию которая создаёт новый стор. Аналог класса для сторов, когда сторы имеют похожие свойства. Они работают только с Map Store. Обязательно есть
value.id: string
. Имя начинается с большой буквы. -
Builder создаётся через функцию
defineMap
. В API все функцииcreate*
создают сразу нужный объект, аdefine*
созвращают Builder, уже который будет создавать нужный объект.export type SomeValue = { id: string }; export const Some = defineMap((store, id) => { // Код запуска return () => { // Код остановки } }) let store = Some(id) // или value = useStore(Some, id)
Готовые компоненты:
- Роутер
- Сохранение с
localStorage
- Сахар для создания сторов, которые зависят от других сторов
- Работа со списками (не готово)
Абстракции синхронизации данных:
-
Обычные Store, Map Store, Builder, роутер и т. д. никак не связаны с Логаксом. Можно даже не добавлять Логакс в проект.
-
Есть SyncMap. Это Map Store с Builder, который реализует CRDT Map (то есть объект ключ-значение, что при конфликте редактирования между пользователями остаётся значение записанное последним). Этот стор уже зависит от Logux Client.
-
Создаётся так:
export const User = defineSyncMap<UserValue>('users')
-
Есть три режима SyncMap — все изменения подтверждаются сервером, подтверждение сервером с оффлайн-кешем, всё храниться только офлайн.
-
У SyncMap хитрый тип у значения стора —
{ isLoading: true } | { isLoading: false, ...StoreValue }
. То есть TypeScript будет заставлять вас проверитьvalue.isLoading
перед тем как обратиться к значениям, так как они могут ещё не успеть загрузиться с сервера. -
Изменения SyncMap делают через функции
changeSyncMap(user, { name: 'New' })
илиdeleteSyncMapById(id)
. Всё ради tree shaking. -
При создании SyncMap стора он отправит на сервер экшен с запросом на подписку
users/1
. Стор сам подпишется на лог Логакса, чтобы следить за получением новых экшенов от других клиентов (например, когда кто-то изменил ключ этого стора). При вызовеchangeSyncMap(user, { name: 'New' })
функция не только изменит ключ стора, но и отправит на сервер экшен с изменением данных. Логакс Сервер уже сохранит его в БД и разошлёт остальным клиентам. -
Работа со SyncMap стором в Реакте выглядит так:
let client = useClient() let userValue = useStore(User, id) if (userValue.isLoading) { return <Name onChange={name => { changeSyncMapById(client, User, id, { name }) }} > {userValue.name} </Name> }
Идём дальше в абстракциях:
-
Можно запрашивать с сервера не только данные одного «пользователя», но и искать по всем пользователям (которая вернёт тоже стор)
let store = createFilter(client, User, { admin: true }, { sortBy: 'name' })
-
В Реакте есть шорткат:
let { isLoading, list } = useFilter(User, { admin: true }, { sortBy: 'name' })
-
Фильтр при создании отправит на сервер подписку на канал
users
, передав{ admin: true }
как фильтр подписки. Так же фильтр начнёт слушать события добавления/удаления/изменения пользователей и менять список соответственно (даже если связи с сервером нет и мы меняет только локальную копию).