Tarjima qoshish uchun havola boyicha o'tib Pull Request oching (havolaga o'tish).
Standart til uchun tarkibni ko'rsatadi.
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 = 1incremented(); // $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.
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 valueconst $counter = createStore(0);// and with explicit typingconst $user = createStore<{ name: "Bob"; age: 25 } | null>(null);const $posts = createStore<Post[]>([]);
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:
- 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>}
<script setup> import { useUnit } from "effector-vue/composition"; import { $counter } from "./model.js";
const counter = useUnit($counter);</script>
import { useUnit } from 'effector-solid'import { $counter } from './model.js'
const Counter = () => { const counter = useUnit($counter)
return <div>{counter()}</div>}
- Since for building your logic outside the UI you may also need store data, you can use the
sample
method and pass the store tosource
, 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.
- 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);});
- 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
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.
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=11incrementedBy(39); // 11+39=50decrementedBy(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 categoryChangedsample({ 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 songsconst $totalSongsCount = combine($author, (author) => author.songs.length);// total number of likesconst $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.
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:
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,});
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.
Related API and docs
-
API
createStore
— Method for creating a storeStore
— Description of a store and its methods
-
Articles
Ingliz tilidagi hujjatlar eng dolzarb hisoblanadi, chunki u effector guruhi tomonidan yozilgan va yangilanadi. Hujjatlarni boshqa tillarga tarjima qilish jamiyat tomonidan kuch va istaklar mavjud bo'lganda amalga oshiriladi.
Esda tutingki, tarjima qilingan maqolalar yangilanmasligi mumkin, shuning uchun eng aniq va dolzarb ma'lumot uchun hujjatlarning asl inglizcha versiyasidan foydalanishni tavsiya etamiz.