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
- Redux не позволяет легко создать несколько независимых хранилищ. Effector позволяет одновременно работать с несколькими хранилищами. Это актуально для больших проектов с миллионов сценариев. Глобальный стор будет сильно увеличивать размер бандла (или основного скрипта приложения), даже если сами сценарии выносить в отдельные чанки и лениво подгружать по мере надобности.
- Redux вынуждает писать много кода, в частности бойлерплейта.
- Redux написан на чистом JavaScript без оглядки на статическую типизацию. В отличие от Effector, изначально написанного на TypeScript.
- Redux имеет более функциональные инструменты для отладки. Для effector существует только консольный логгер.
Вывод
Скажу очевидную вещь, но effector — это не серебряная пуля, и не стоит слепо её применять. Но это отличный новый взгляд на управление состоянием.