За обновлениями можно следить в telegram-канале https://t.me/quasiart

Effector — это новый реактивный стейт менеджер. Эта статья о том, почему стоит выбрать effector: что такое, как работает и чем лучше или хуже редакса.

Основы

Effector использует знакомые всем концепты: store (хранилище) и event (событие).

store — это объект, хранящий значение.

import {createStore} from 'effector'

const counter = createStore(0) // создание хранилища со значением 0 по умолчанию

counter.watch(console.log) // слежение за изменением хранилища

event — метод для обновления значения. Event — это что-то вроде action в терминологии Redux, а store.on(trigger, handler) — это что-то вроде createStore(reducer). Это простая функция, которую можно запустить из любого места в коде проекта.

import {createStore, createEvent} from 'effector'

const increment = createEvent('increment')
const decrement = createEvent('decrement')
const resetCounter = createEvent('reset counter')

const counter = createStore(0)
  .on(increment, state => state + 1) // подписываемся на событие и возвращаем новое значение хранилища
  .on(decrement, state => state - 1)  
  .reset(resetCounter)

counter.watch(console.log)

События и хранилища являются реактивными сущностями, у которых есть метод watch, позволяющий подписываться на события и изменения хранилища.

Интеграция с React

Компонент можно связать с хранилищем с помощью хука useStore из пакета effector-react.

import React from 'react'
import ReactDOM from 'react-dom'
import {createStore, createEvent} from 'effector'
import {useStore} from 'effector-react'

const increment = createEvent('increment')
const decrement = createEvent('decrement')
const resetCounter = createEvent('reset counter')

const counter = createStore(0)
  .on(increment, state => state + 1)
  .on(decrement, state => state - 1)
  .reset(resetCounter)

counter.watch(console.log)

const Counter = () => {
  const value = useStore(counter) // подписка на измеения хранилища

  return (
    <>
      <div>Count: {value}</div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={resetCounter}>reset</button>
    </>
  )
}

const App = () => <Counter />
const div = document.createElement('div')
document.body.appendChild(div)
ReactDOM.render(
  <App/>,
  div
)

Интеграция с другими фреймворками

Effector можно интегрировать и с другими фреймворками, например, Vue, Svelte и пр.

Побочные эффекты (Side Effects)

С effector больше не нужны различные redux-thunk или redux-saga для обработки сайд эффектов.

В effector есть хэлпер createEffect, который оборачивает асинхронную функцию и создаёт 3 события, на которые хранилище может подписаться: инициализатор (непосредственно эффект) и два резолвера: done and fail.

const getUser = createEffect('get user', {
    handler: params => fetch(`https://example.com/get-user/${params.id}`)
        .then(res => res.json())
})

const users = createStore([]) // Значение по умолчанию
    // getUser.done -- это событие, вызываемое как только промис,
    // возвращённый эффектом, становится разрешённым (resolved)
    .on(getUser.done, (state, {result, params}) => [...state, result])

Расширенное использование: combine и map

В effector можно создавать computed store (вычисляемые хранилища). Их можно создавать с помощью метода combine или метода хранилища map.

combine создаёт новый store, который рассчитывается из существующих хранилищ:

const balance = createStore(0)
const username = createStore('Vasya')

const greeting = combine(balance, username, (balance, username) => {
  return `Hello, ${username}. Your balance is ${balance}`
})

greeting.watch(data => console.log(data)) // Hello, Vasya. Your balance is 0

map создаёт производные хранилища:

const title = createStore("")
const changed = createEvent()

const length = title.map((title) => title.length)

title.on(changed, (oldTitle, newTitle) => newTitle)

length.watch((length) => console.log("new length is ", length)) // new length is 0

changed("hello") // new length is 5
changed("world")
changed("hello world") // new length is 11

Сравнение с Redux

  1. Redux не позволяет легко создать несколько независимых хранилищ. Effector позволяет одновременно работать с несколькими хранилищами. Это актуально для больших проектов с миллионов сценариев. Глобальный стор будет сильно увеличивать размер бандла (или основного скрипта приложения), даже если сами сценарии выносить в отдельные чанки и лениво подгружать по мере надобности.
  2. Redux вынуждает писать много кода, в частности бойлерплейта.
  3. Redux написан на чистом JavaScript без оглядки на статическую типизацию. В отличие от Effector, изначально написанного на TypeScript.
  4. Redux имеет более функциональные инструменты для отладки. Для effector существует только консольный логгер.

Вывод

Скажу очевидную вещь, но effector — это не серебряная пуля, и не стоит слепо её применять. Но это отличный новый взгляд на управление состоянием.

Ссылки