DEV Community

Cover image for Why do I choose Effector instead of Redux or MobX?
Ilya Lesik for  brainhub

Posted on • Edited on

Why do I choose Effector instead of Redux or MobX?

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Links

Top comments (4)

Collapse
 
jt3k profile image
Andrey Gurtovoy

it looks like jQuery

import $ from 'jquery'

const increment = 'increment'
const decrement = 'decrement'
const resetCounter = 'reset counter'

let counter = 0;

bus = $({})
  .on(increment, () => {counter += 1})
  .on(decrement, () => {counter -= 1})  
  .on(resetCounter, () => {counter = 0})

///////////////////////////////
bus.triggerHandler(increment)
bus.triggerHandler(increment)
bus.triggerHandler(decrement)
bus.triggerHandler(resetCounter)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
d9k profile image
Dmitry

createEffect() + createStore().on() reminds redux-toolkit createSlice().

And combine() reminds reselect

Collapse
 
dangdennis profile image
Dennis Dang

This has bucklescript types. Sold

Collapse
 
erixtekila profile image
Eric Priou

Reminds me Undux.
undux.org/