Skip to content

Instantly share code, notes, and snippets.

@ai
Last active May 11, 2021 19:35
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ai/cb700cc1679aeb253818341efaefcba3 to your computer and use it in GitHub Desktop.
Save ai/cb700cc1679aeb253818341efaefcba3 to your computer and use it in GitHub Desktop.

Краткая характеристика:

  1. У него много сторов и сторы могут зависеть друг от друга, а не один большой стор и селекторы. То есть он ближе к Эфектору, чем в Редаксу/MobX. Всё ради tree shaking.
  2. Он ближе к стору прямых измений. В публичном API нет экшенов. Но всё-таки value = 1 на манер MobX запрещены — значения можно менять только через спец. методы. И в синхронизации состояния с сервером экшены есть (просто скрыты из публичного API).

Плюсы:

  1. Может работать без Логакса, чисто как стейт-менеджер.
  2. API специально создан, чтобы хранить в сторах бизнес-логику, чем разгружать компоненты и упрощать переносимость приложения между фреймворками.
  3. От 157 байт (!) в вашем JS-бандле.
  4. Расчитан на агрессивный tree shaking, чтобы в JS-бандле был только код того состояния, которые используются в текущих страницах.
  5. Очень ленивый — сторы на которых никто не подписан выгружаются из памяти, а их бизнес-логика останавливается.
  6. Стейт-менеджер написан так, чтобы прятать сложную логику работы с сервером. Особенно когда нужен CRDT, разрешение конфликтов редактирования, синхронизация от веб-сокету. Сразу содержит в себе опциональные умные сторы работы через Логакс.

Базовыне абстракции стора:

  1. Идея Logux State, что сторы — умные, они сами подписываются за событиями системы, содержат логику и валидации. Например, стор роутера сам следит за window.onpopstate и за кликами по ссылкам. Идея вынести максимум логики из Реакт-компонентов в сторы, так как сторы легче тестировать и переносить между UI-фреймворками.

  2. Особенность Logux State, то у сторов есть два состояния — остановлен и запущен (когда стор установил всех подписчиков). Сторы — ленивые. Когда появляется первый подписчик на изменения стора, он запускается (сам подписывается на события системы). Когда отписывается последний подписчик — стор останавливается.

  3. У стора есть значение. Он значение появляется только когда стор запущен. Поэтому единственный способ прочитать значение — либо подписаться value = useStore(store) / store.listen(value => {}), либо вызвать getValue(store).

  4. Стор создаётся через:

    export type StoreValue = number;
    export const store = createStore<StoreValue>(() => {
      // Код запуска
      return () => {
        // Код остановки
      }
    })
  5. Менять всё значение стора можно через store.set(newValue). Но если вызвать метод на остановленом сторе, то новое значение будет проигнорировано.

  6. Какие-то операции над сторами надо выносить в функции, а не методы сторы. doSomthing(store), но НЕ store.doSomething(). Всё ради tree-shaking.

  7. Но некоторые сторы могут что-то добавлять в объект стора store.x.

Расширяем абстракции стора:

  1. Есть Map Store. Создаётся через createMap. У него значением может быть только объект. Появляется метод store.setKey(key, value). store.listen((value, changedKey) => {}) принимает ключ, который изменился.

  2. У Map Store значение — ссылка всегда на один и тот же объект. Вызов setKey или set меняет ключи в этом объекте, не меняя сам объект.

  3. Store Builder — функцию которая создаёт новый стор. Аналог класса для сторов, когда сторы имеют похожие свойства. Они работают только с Map Store. Обязательно есть value.id: string. Имя начинается с большой буквы.

  4. 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)

Готовые компоненты:

  1. Роутер
  2. Сохранение с localStorage
  3. Сахар для создания сторов, которые зависят от других сторов
  4. Работа со списками (не готово)

Абстракции синхронизации данных:

  1. Обычные Store, Map Store, Builder, роутер и т. д. никак не связаны с Логаксом. Можно даже не добавлять Логакс в проект.

  2. Есть SyncMap. Это Map Store с Builder, который реализует CRDT Map (то есть объект ключ-значение, что при конфликте редактирования между пользователями остаётся значение записанное последним). Этот стор уже зависит от Logux Client.

  3. Создаётся так:

    export const User = defineSyncMap<UserValue>('users')
  4. Есть три режима SyncMap — все изменения подтверждаются сервером, подтверждение сервером с оффлайн-кешем, всё храниться только офлайн.

  5. У SyncMap хитрый тип у значения стора — { isLoading: true } | { isLoading: false, ...StoreValue }. То есть TypeScript будет заставлять вас проверить value.isLoading перед тем как обратиться к значениям, так как они могут ещё не успеть загрузиться с сервера.

  6. Изменения SyncMap делают через функции changeSyncMap(user, { name: 'New' }) или deleteSyncMapById(id). Всё ради tree shaking.

  7. При создании SyncMap стора он отправит на сервер экшен с запросом на подписку users/1. Стор сам подпишется на лог Логакса, чтобы следить за получением новых экшенов от других клиентов (например, когда кто-то изменил ключ этого стора). При вызове changeSyncMap(user, { name: 'New' }) функция не только изменит ключ стора, но и отправит на сервер экшен с изменением данных. Логакс Сервер уже сохранит его в БД и разошлёт остальным клиентам.

  8. Работа со SyncMap стором в Реакте выглядит так:

    let client = useClient()
    let userValue = useStore(User, id)
    if (userValue.isLoading) {
      return <Name
               onChange={name => {
                 changeSyncMapById(client, User, id, { name })
               }}
             >
               {userValue.name}
             </Name>
    }

Идём дальше в абстракциях:

  1. Можно запрашивать с сервера не только данные одного «пользователя», но и искать по всем пользователям (которая вернёт тоже стор)

    let store = createFilter(client, User, { admin: true }, { sortBy: 'name' })
  2. В Реакте есть шорткат:

    let { isLoading, list } = useFilter(User, { admin: true }, { sortBy: 'name' })
  3. Фильтр при создании отправит на сервер подписку на канал users, передав { admin: true } как фильтр подписки. Так же фильтр начнёт слушать события добавления/удаления/изменения пользователей и менять список соответственно (даже если связи с сервером нет и мы меняет только локальную копию).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment