{"id":14291,"url":"\/distributions\/14291\/click?bit=1&hash=257d5375fbb462be671b713a7a4184bd5d4f9c6ce46e0d204104db0e88eadadd","hash":"257d5375fbb462be671b713a7a4184bd5d4f9c6ce46e0d204104db0e88eadadd","title":"\u0420\u0435\u043a\u043b\u0430\u043c\u0430 \u043d\u0430 Ozon \u0434\u043b\u044f \u0442\u0435\u0445, \u043a\u0442\u043e \u043d\u0438\u0447\u0435\u0433\u043e \u0442\u0430\u043c \u043d\u0435 \u043f\u0440\u043e\u0434\u0430\u0451\u0442","buttonText":"","imageUuid":""}

Управление состоянием - Effector

Привет, на связи Antihype JS. Продолжаем цикл статей по разбору стейт менеджеров. Сегодня у нас на обзоре наделавший шуму в сообществе Effector.js.

В отличие от других подобных библиотек, команда Effector позиционирует свое решение не как стейт менеджер, а как инструмент для описания бизнес-логики приложения.

Effector предлагает возможность описать бизнес-логику на том же языке, на котором общается команда разработчиков продукта, используя базовые примитивы: Event, Store, Effect.

Библиотека является framework agnostic решением, может работать с React, Vue и даже Solid.js, используя соответствующие binding пакеты.

Effector построен вокруг концепции связи между атомарными сторами и событиями, использует иммутабельный подход к данным и имеет хорошую поддержку TypeScript.

Примитивы Effector

Store - основной объект для хранения данных. Рекомендуется декомпозировать ваши данные на большое количество сторов и связывать при необходимости. По конвенции имена всех сторов начинаются с $.

import { createStore } from 'effector' type User = { name: string } const $user = createStore<User | null>(null)

Event - способ сообщить о каком-либо событии в приложении или просто изменить состояние стора. При срабатывании запускает каскад последующих вычислений в приложении.

import { createEvent } from 'effector' type User = { name: string } const userCreated = createEvent<User>()

Effect - контейнер для всевозможных сайд-эффектов. Как правило эффекты используются для асинхронных операций. Эффект имеет встроенные события успешного завершения, ошибки, а также индикатор того что эффект находится в процессе выполнения. В качестве первого аргумента принимает обычную функцию:

import { createEffect } from 'effector' type User = { name: string } const createUser = () => new Promise<User>((res) => res({name: 'user'})) const createUserFx = createEffect<void, User>(createUser) // событие успешного завершения, отдает данные createUserFx.doneData // событие неуспешного завершения, отдает объект ошибки createUserFx.failData // boolean стор индикатор // содержит true когда хотя бы 1 createUserFx // находится в процессе выполнения createUserFx.pending

Связи между примитивами и написание логики

Мы разобрали необходимые примитивы для работы с Effector. Давайте напишем небольшой пример программы, которая будет отправлять состояние счетчика на сервер, когда его значение будет равно 5.

Для начала создадим стор для нашего счетчика и событие инкремента:

import { createStore, createEvent } from 'effector' const inc = createEvent() const $counter = createStore<number>(0)

Чтобы изменить состояние стора по событию мы будем использовать метод on. Для логирования состояния в консоль будем использовать метод watch:

import { createStore, createEvent } from 'effector' const inc = createEvent() const $counter = createStore<number>(0) $counter.on(inc, (counter) => counter + 1) $counter.watch((counter) => console.log('counter:', counter)) inc() inc() // В консоли: // counter: 0 // counter: 1 // counter: 2

Метод on принимает событие и функцию для обновления состояния. При срабатывании события inc:

  • Запускается функция для обновления стейта с предыдущим значением стора.
  • После обновления состояния стора срабатывает метод watch, происходит вывод текущего значения стейта в консоль.

Теперь добавим эффект сохранения состояния счетчика на базе фейковой функции. Логируем приходящий аргумент и резолвим промис через 500 миллисекунд для имитации выполнения запроса:

import { createStore, createEvent, createEffect } from 'effector' const inc = createEvent() const saveCountFx = createEffect((count: number) => { return new Promise((res) => { console.log('saveCountFx:', count) setTimeout(res, 500) }) }) const $counter = createStore<number>(0) $counter.on(inc, (counter) => counter + 1) $counter.watch((counter) => console.log('counter:', counter))

Осталось добавить ключевой элемент всей библиотеки - оператор sample. Этот метод позволяет запускать какую-либо логику при срабатывании событий. В нашем случае, нам необходимо запустить эффект при состоянии счетчика равном 5:

sample({ source: $counter, filter: (counter) => counter === 5, target: saveCountFx, })

В качестве источника данных source используется наш стор $counter. В качестве сущности для последующего запуска target используется эффект saveCountFx. Для соблюдения условия запуска используется свойство filter. В filter передается функция, которая получает текущее значение source. Такую запись можно перефразировать следующим образом:

Когда $counter обновит состояние, то проверить, что его значение равно 5. Если условие выполняется, то запустить saveCountFx со значением из $counter.

Также с помощью sample можно задать событие clock, по которому следует запускать нашу логику:

sample({ source: $counter, clock: buttonClicked, filter: (counter) => counter === 5, target: saveCountFx, })

Когда событие buttonClicked будет вызвано, то проверить что значение $counter равно 5. Если условие выполняется, то запустить saveCountFx со значением из $counter.

Наша задача с сохранением значения счетчика будет выглядеть следующим образом:

import { createStore, createEvent, createEffect, sample } from 'effector' const inc = createEvent() const saveCountFx = createEffect((count: number) => { return new Promise((res) => { console.log('saveCountFx:', count) setTimeout(res, 500) }) }) const $counter = createStore<number>(0) $counter.on(inc, (counter) => counter + 1) sample({ source: $counter, filter: (counter) => counter === 5, target: saveCountFx, }) saveCountFx.doneData.watch(() => console.log('doneData')) saveCountFx.pending.watch((pending) => console.log('pending:', pending)) inc() inc() inc() inc() inc()

Мы добавили вызов события инкремента 5 раз, подписки на события выполнения и индикатор выполнения операции. Результат в консоли:

// pending: false // pending: true // saveCountFx: 5 // doneData // pending: false

Тестирование, SSR, экосистема

  • Полный гайд от сообщества по тестированию с Effector
  • Пакет для SSR на Next.js
  • Репозиторий с дополнительными пакетами для Effector от сообщества: формы, eslint плагины, библиотеки утилит

Заключение

Мы сделали обзор на основные концепции и работу с библиотекой Effector. Использовать его в проде или нет - как обычно, решать только вам. Важно отметить, что Effector имеет большое русскоязычное сообщество.

Если вам понравился контент и вы интересуетесь фронтенд-разработкой, то подписывайтесь на наш телеграм-канал.

0
3 комментария
Routy Rout

Братан, хорош, давай, давай, вперёд! Контент в кайф, можно ещё? Вообще красавчик! Можно вот этого вот почаще?

Ответить
Развернуть ветку
Иванов Иван

Братан, хорош

Ответить
Развернуть ветку
MedievalRain

Братан, хорош, давай, давай, вперёд! Контент в кайф, можно ещё? Вообще красавчик! Можно вот этого вот почаще?

Ответить
Развернуть ветку
0 комментариев
Раскрывать всегда