State management

All state management is done using stores, and the key feature is that stores do not have the usual setState. A store updates reactively when the event it is subscribed to is triggered, for example:

import { createStore, createEvent } from "effector";
const $counter = createStore(0);
const incremented = createEvent();
// when incremented triggered, increase the counter by 1
$counter.on(incremented, (counterValue) => counterValue + 1);
incremented(); // $counter = 1
incremented(); // $counter = 2

If you are not familiar with events yet, just think of them as a trigger for updating the store. You can learn more about events on the events page, as well as how to think in the effector paradigm and why events matter.

Data Immutability

If you store a reference type, such as an array or an object, in a store, then to update such a store you can either use immer or first create a new instance of that type:

// ✅ all good
// update array
$users.on(userAdded, (users, newUser) => {
const updatedUsers = [...users];
updatedUsers.push(newUser);
return updatedUsers;
});
// update object
$user.on(nameChanged, (user, newName) => {
const updatedUser = { ...user };
updatedUser.name = newName;
return updatedUser;
});
// ❌ this is bad
$users.on(userAdded, (users, newUser) => {
users.push(newUser); // mutate array
return users;
});
$user.on(nameChanged, (user, newName) => {
user.name = newName; // mutate object
return user;
});

Store creation

Creating a store is done using the createStore method:

import { createStore } from "effector";
// creating a store with an initial value
const $counter = createStore(0);
// and with explicit typing
const $user = createStore<{ name: "Bob"; age: 25 } | null>(null);
const $posts = createStore<Post[]>([]);
Naming stores

The effector team suggests using the $ prefix for stores, as it improves code readability and IDE autocompletion.

Reading values

As you already know, effector is a reactive state manager, and a store is a reactive unit — reactivity is not created in a magical way. If you try to just use a store, for example:

import { createStore } from "effector";
const $counter = createStore(0);
console.log($counter);

You will see an obscure object with a bunch of properties, which effector needs for correct operation, but not the current value. To get the current value of a store, there are several ways:

  1. Most likely you are also using some framework like React, Vue, or Solid, and in that case you need an adapter for this framework: effector-react, effector-vue, or effector-solid. Each of these packages provides the useUnit hook to get data from a store and subscribe to its changes. When working with UI, this is the only correct way to read data:
import { useUnit } from 'effector-react'
import { $counter } from './model.js'
const Counter = () => {
const counter = useUnit($counter)
return <div>{counter}</div>
}
  1. Since for building your logic outside the UI you may also need store data, you can use the sample method and pass the store to source, for example:
import { createStore, createEvent, sample } from "effector";
const $counter = createStore(0);
const incremented = createEvent();
sample({
clock: incremented,
source: $counter,
fn: (counter) => {
console.log("Counter value:", counter);
},
});
incremented();

A bit later we will also discuss the sample method and how it can be used with stores.

  1. You can subscribe to store changes using watch, however this is mainly used for debugging or for some custom integrations:
$counter.watch((counter) => {
console.log("Counter changed:", counter);
});
  1. The getState() method is generally used only for working with low-level APIs or integrations. Try not to use it in your code, as it may lead to race conditions:
console.log($counter.getState()); // 0
Why not use getState?

For effector to work correctly with reactivity, it needs to build connections between units so that the data is always up to date. In the case of .getState(), we essentially break this system and take the data from the outside.

Store updates

As mentioned earlier, state updates happen through events. A store can subscribe to events using the .on method — good for primitive reactions, or the sample operator — which allows updating a store based on another store or filtering updates.

What is sample?

The sample method is an operator for creating connections between units. With it, you can trigger events or effects, as well as write new values into stores. Its algorithm is simple:

const trigger = createEvent();
const log = createEvent<string>();
sample({
clock: trigger, // 1. when trigger fires
source: $counter, // 2. take the value from $counter
filter: (counter) => counter % 2 === 0, // 3. if the value is even
fn: (counter) => "Counter is even: " + counter, // 4. transform it
target: log, // 5. call and pass to log
});

Using .on

With .on, we can update a store in a primitive way: event triggered → call the callback → update the store with the returned value:

import { createStore, createEvent } from "effector";
const $counter = createStore(0);
const incrementedBy = createEvent<number>();
const decrementedBy = createEvent<number>();
$counter.on(incrementedBy, (counterValue, delta) => counterValue + delta);
$counter.on(decrementedBy, (counterValue, delta) => counterValue - delta);
incrementedBy(11); // 0+11=11
incrementedBy(39); // 11+39=50
decrementedBy(25); // 50-25=25

Using sample

With the sample method, we can update a store in a primitive way:

import { sample } from "effector";
sample({
clock: incrementedBy, // when incrementedBy is triggered
source: $counter, // take data from $counter
fn: (counter, delta) => counter + delta, // call fn
target: $counter, // update $counter with the value returned from fn
});
sample({
clock: decrementedBy, // when decrementedBy is triggered
source: $counter, // take data from $counter
fn: (counter, delta) => counter - delta, // call fn
target: $counter, // update $counter with the value returned from fn
});

At the same time, we also have more flexible ways — for example, updating a store only when another store has the required value. For example, perform a search only when $isSearchEnabled is true:

import { createStore, createEvent, sample } from "effector";
const $isSearchEnabled = createStore(false);
const $searchQuery = createStore("");
const $searchResults = createStore<string[]>([]);
const searchTriggered = createEvent();
sample({
clock: searchTriggered, // when searchTriggered is triggered
source: $searchQuery, // take data from $searchQuery
filter: $isSearchEnabled, // continue only if search is enabled
fn: (query) => {
// simulate a search
return ["result1", "result2"].filter((item) => item.includes(query));
},
target: $searchResults, // update $searchResults with the value returned from fn
});

Note that when passing a store into target, its previous value will be fully replaced with the value returned from fn.

Updating from multiple events

A store is not limited to a single event subscription — you can subscribe to as many events as needed, and multiple stores can subscribe to the same event:

import { createEvent, createStore, sample } from "effector";
const $lastUsedFilter = createStore<string | null>(null);
const $filters = createStore({
category: "all",
searchQuery: "",
});
const categoryChanged = createEvent<string>();
const searchQueryChanged = createEvent<string>();
// two different stores subscribing to the same event
$lastUsedFilter.on(categoryChanged, (_, category) => category);
sample({
clock: categoryChanged,
source: $filters,
fn: (filters, category) => ({
// following immutability principles
...filters,
category,
}),
// the result of fn will replace the previous value in $filters
target: $filters,
});
// store subscribing to two different events: searchQueryChanged and categoryChanged
sample({
clock: searchQueryChanged,
source: $filters,
fn: (filters, searchQuery) => ({
// following immutability principles
...filters,
searchQuery,
}),
// the result of fn will replace the previous value in $filters
target: $filters,
});

Here we subscribed two stores to the same categoryChanged event, and also subscribed the $filters store to another event searchQueryChanged.

Derived stores

A derived store is computed based on other stores and automatically updates when those stores change. Imagine we have the following store:

import { createStore } from "effector";
const $author = createStore({
name: "Hanz Zimmer",
songs: [
{ title: "Time", likes: 123 },
{ title: "Cornfield Chase", likes: 97 },
{ title: "Dream is Collapsing", likes: 33 },
],
});

And we want to display the total number of likes, as well as the number of songs for this author. Of course, we could just use this store in the UI with the useUnit hook and calculate those values directly in the component. But this is not the right approach, because we would be mixing logic inside the component and spreading it throughout the application, making the code harder to maintain in the future. And if we wanted to reuse the same logic elsewhere, we’d have to duplicate the code.

In this case, the correct approach is to create derived stores based on $author using the combine method:

import { createStore, combine } from "effector";
const $author = createStore({
name: "Hanz Zimmer",
songs: [
{ title: "Time", likes: 123 },
{ title: "Cornfield Chase", likes: 97 },
{ title: "Dream is Collapsing", likes: 33 },
],
});
// total number of songs
const $totalSongsCount = combine($author, (author) => author.songs.length);
// total number of likes
const $totalLikesCount = combine($author, (author) =>
author.songs.reduce((acc, song) => acc + song.likes, 0),
);

Each of these derived stores will automatically update whenever the original $author store changes.

Important about derived stores!

Derived stores automatically update when the source stores change. They cannot be passed as a target in sample or subscribed to with .on.

At the same time, there can be as many source stores as needed, which allows you, for example, to compute the current application state:

import { combine, createStore } from "effector";
const $isLoading = createStore(false);
const $isSuccess = createStore(false);
const $error = createStore<string | null>(null);
const $isAppReady = combine($isLoading, $isSuccess, $error, (isLoading, isSuccess, error) => {
return !isLoading && isSuccess && !error;
});

undefined values

If you try to use a store value as undefined or put this value into a store:

const $store = createStore(0).on(event, (_, newValue) => {
if (newValue % 2 === 0) {
return undefined;
}
return newValue;
});

you will encounter an error in the console:

Terminal window
store: undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option

By default, returning undefined acts as a command “nothing happened, skip this update”. If you really need to use undefined as a valid value, you must explicitly specify it with the skipVoid: false option when creating a store:

import { createStore } from "effector";
const $store = createStore(0, {
skipVoid: false,
});
The future of undefined

In upcoming versions this behavior will be changed. As practice shows, it’s usually better to just return the previous store value to avoid updating it.

Contributors