Effector is a brand new reactive state manager. Its ambitious team aims to solve all the problems that existing solutions have. Writing the core of the library from scratch took several attempts across six months, and recently the team released the first stable release.
In this article, I will show why I prefer using Effector for my new projects instead of other state managers. Let's get started with the Effector API.
Basics
Effector uses two concepts you might already be familiar with: store and event.
A store is an object that holds some value. We can create stores with the createStore
helper:
import {createStore} from 'effector'
const counter = createStore(0) // create store with zero as default value
counter.watch(console.log) // watch store changes
Stores are lightweight, so whenever you need to introduce some state to your app, you simply create a new store.
So how do we update our store? Events! You create events with the createEvent
helper and have your store updated by reacting on them:
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) // subscribe to the event and return new store value
.on(decrement, state => state - 1)
.reset(resetCounter)
counter.watch(console.log)
Event is like an "action" in Redux terms, and store.on(trigger, handler)
is somewhat like createStore(reducer)
. Events are just functions that can be called from any place in your code.
Effector implements the Reactive Programming paradigm. Events and stores are considered as reactive entities (streams, in other words), they have a watch
method which allows subscribing to events and store changes.
Integration with React
A component can connect to the store by calling the useStore
hook from effector-react
package. Effector events can be passed to child React elements as event handlers (onClick
, etc.)
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) // subscribe to store changes
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
)
Integration with other frameworks
Vue
There is effector-vue package.
Svelte
Effector stores are Observable, so you don't need any additional packages to use them in Svelte. Simply prepend $
to the store's name in your template:
// Counter.svelte
<script context="module">
import effector from 'effector/effector.umd.js';
export const increment = createEvent('increment')
export const decrement = createEvent('decrement')
export const resetCounter = createEvent('reset counter')
export const counter = effector.createStore(0)
.on(increment, (n) => n + 1)
.on(decrement, state => state - 1)
.reset(resetCounter)
</script>
// App.svelte
<script>
import { counter, increment, decrement, resetCounter } from './Counter.svelte'
</script>
<div>Count: {$counter}</div>
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>
<button on:click={resetCounter}>reset</button>
Side effects
With Effector you don't need thunks or sagas to handle side effects. Effector has a convenient helper called createEffect
that wraps an async function and creates three events that your store can subscribe to: an initializer (the effect itself) and two resolvers called done
and fail
.
const getUser = createEffect('get user');
getUser.use(params => {
return fetch(`https://example.com/get-user/${params.id}`)
.then(res => res.json())
})
// OR
const getUser = createEffect('get user', {
handler: params => fetch(`https://example.com/get-user/${params.id}`)
.then(res => res.json())
})
const users = createStore([]) // <-- Default state
// getUser.done is the event that fires whenever a promise returned by the effect is resolved
.on(getUser.done, (state, {result, params}) => [...state, result])
Advanced usage: combine
, map
One of the awesome features of Effector is computed stores. Computed stores can be created using either the combine
helper or .map
method of the store. This allows subscribing only to changes that matter to the particular component. In React apps, performance may be heavily impacted by unnecessary state updates, so Effector helps eliminate them.
combine
creates a new store that calculates its state from several existing stores:
const balance = createStore(0)
const username = createStore('zerobias')
const greeting = combine(balance, username, (balance, username) => {
return `Hello, ${username}. Your balance is ${balance}`
})
greeting.watch(data => console.log(data)) // Hello, zerobias. Your balance is 0
map
allows creating derived stores:
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
Comparison with other state managers
Redux
- Most projects that use Redux implement the whole application state in a single store. Having multiple stores isn't forbidden, but doing this right is kind of tricky. Effector is built to work with lots of different stores simultaneously.
- Redux is very explicit but very verbose as well. Effector requires less boilerplate code, but all state dependencies are still explicit.
- Redux was originally written in pure JS and without static typing in mind. Effector has much wider typing support out of the box, including type inference for most helpers and methods.
- Redux has great dev tools. Effector somewhat lags right now, but the team already has plans for dev tools that visually represent your application as a graph of connected stores and events.
MobX
- When minified and gzipped, MobX is almost 20kb (14.9kb + 4.6kb for React bindings), while Effector is less than 8 kb (5.8 kb + 1.7 kb for React).
- MobX has a lot of magic inside: implicit subscriptions to observable data changes, "mutable" state objects that use Proxies under the hood to distribute updates, etc. Effector uses immutable state, explicitly combines stores' state and only allows changing it through events.
- MobX encourages keeping your data model close to the view. With Effector, you can completely isolate the data model and keep your UI components' API clean & simple.
- May be difficult to use with custom data-structures.
RxJS
- Strictly speaking, although RxJS solves many tasks, it's a reactive extensions library, not a state management tool. Effector, on the other hand, is designed specifically for managing application state and has a small API that is easy to learn.
- RxJS is not 'glitch-free'. In particular, synchronous streams for computed data do not produce consistent updates by default: see an example on how different reactive state management tools handle this task.
Why did I choose Effector
Here's a list of things I consider to be Effector's advantages over most similar tools:
- Expressive and laconic API.
- Reactive programming paradigm at its core.
- Stable, production-ready.
- Great performance, also I don't see any memory leaks.
- Motivated team, great community.
Conclusion
Effector is not a silver bullet, but it's certainly a fresh take on state management. Do not be afraid to try something new and diverge from the most popular solutions. Interested? Try Effector now!
Thanks
- Andrey Sitnik @ai - article promotion
- Alexander Kladkov @A1992 - fact checking
- Artyom Arutyunyan @artalar - fact checking
- Alexander Chudesnov - proofreading, editing
Top comments (4)
it looks like jQuery
createEffect()
+createStore().on()
reminds redux-toolkitcreateSlice()
.And
combine()
reminds reselectThis has bucklescript types. Sold
Reminds me Undux.
undux.org/