Управление состоянием
Вся работа с состоянием происходит с помощью сторов, и ключевая особенность, что у сторов нету привычного setState
. Стор обновляется реактивно при срабатывании события, на которое он подписывается, например:
import { createStore, createEvent } from "effector";
const $counter = createStore(0);
const incremented = createEvent();
// при каждом вызове incremented отработает переданный колбэк$counter.on(incremented, (counterValue) => counterValue + 1);
incremented(); // $counter = 1incremented(); // $counter = 2
Если вы не знакомы с событиями, то пока воспринимайте их просто как триггер для обновления стора. Подробнее о событиях можно узнать на странице события, а также как думать в парадигме effector, и почему события важны.
Если вы храните в сторе ссылочный тип данных, например массив или объект, то для обновления такого стора вы можете использовать immer или сначала создать новый инстанс этого типа:
// ✅ Все круто
// обновление массива$users.on(userAdded, (users, newUser) => { const updatedUsers = [...users]; updatedUsers.push(newUser); return updatedUsers;});
// обновление объекта$user.on(nameChanged, (user, newName) => { const updatedUser = { ...user }; updatedUser.name = newName; return updatedUser;});
// ❌ А тут все плохо
$users.on(userAdded, (users, newUser) => { users.push(newUser); // мутируем массив return users;});
$user.on(nameChanged, (user, newName) => { user.name = newName; // мутируем объект return user;});
Создание стора
Создание стора происходит при помощи метода createStore
:
import { createStore } from "effector";
// создание стора с начальным значениемconst $counter = createStore(0);// и с явной типизациейconst $user = createStore<{ name: "Bob"; age: 25 } | null>(null);const $posts = createStore<Post[]>([]);
Команда effector предлагает использовать префикс $
для сторов, поскольку это улучшает ориентацию в коде и автокомплит в IDE.
Чтение значений
Как вы уже знаете effector это реактивный стейт менеджер, а стор – реактивный юнит и реактивность создается не магическим образом. Если вы попробуете просто использовать стор, например:
import { createStore } from "effector";
const $counter = createStore(0);console.log($counter);
Вы увидите непонятный объект с кучей свойств, который необходим effector для корректной работы, но не текущее значение. Чтобы получить текущее значение стора, есть несколько способов:
- Скорее всего вы также используете какой-нибудь фреймворк React, Vue или Solid и тогда вам нужен адаптер под этот фреймворк effector-react, effector-vue или effector-solid. Каждый из этих пакетов предоставляет хук
useUnit
для получения данных из стора, а также подписки на его изменения. При работе с UI это единственный верный способ для чтения данных:
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>}
- Поскольку для построения вашей логики вне UI вам также может понадобится данные стора, вы можете использовать метод
sample
и передать стор вsource
, например:
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();
Мы чуть попозже еще обсудим метод sample
и как можно с ним работать используя сторы.
- Можно подписаться на изменения стора через
watch
, однако это используется скорее для дебага либо каких-то самописных интеграций:
$counter.watch((counter) => { console.log("Counter changed:", counter);});
- Метод
getState()
, используется, как правило, только для работы с низкоуровневым API или интеграций. Старайтесь не использовать его в вашем коде, потому что может привести к гонке данных:
console.log($counter.getState()); // 0
Чтобы effector корректно работал с реактивностью ему необходимо построить связи между юнитами, чтобы всегда были актуальные данные. В случае .getState()
мы как бы ломаем эту систему и берем данные извне.
Обновление состояния
Как говорилось ранее, обновление состояния происходит при помощи событий. Можно подписаться стором на события с помощью метода .on
– хорош для примитивных реакций, или оператора sample
– позволяет обновить стор в зависимости от другого стора, или фильтровать обновления.
Метод sample
это оператор для связи между юнитами, с его помощью можно вызывать события или эффекты, а также записывать в сторы новые значения. Алгоритм его работы простой:
const trigger = createEvent();const log = createEvent<string>();
sample({ clock: trigger, // 1. когда trigger сработает source: $counter, // 2. возьми значение из $counter filter: (counter) => counter % 2 === 0, // 3. если значение четное fn: (counter) => "Counter is even: " + counter, // 4. преобразуй его target: log, // 5. вызови и передай в log});
С помощью .on
С помощью .on
мы можем обновить стор примитивным способом: вызвалось событие -> вызови колбэк -> обнови стор возвращаемым значением:
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
С помощью sample
С методом sample
мы можем как примитивно обновить стор:
import { sample } from "effector";
sample({ clock: incrementedBy, // когда сработает incrementedBy source: $counter, // возьми данные из $counter fn: (counter, delta) => counter + delta, // вызови колбэк fn target: $counter, // обнови $counter возвращаемым значением из fn});
sample({ clock: decrementedBy, // когда сработает decrementedBy source: $counter, // возьми данные из $counter fn: (counter, delta) => counter - delta, // вызови колбэк fn target: $counter, // обнови $counter возвращаемым значением из fn});
так и имеем более гибкие способы, например обновить стор только когда другой стор имеет нужное значение, к примеру искать только когда $isSearchEnabled
имеет значение true
:
import { createStore, createEvent, sample } from "effector";
const $isSearchEnabled = createStore(false);const $searchQuery = createStore("");const $searchResults = createStore<string[]>([]);
const searchTriggered = createEvent();
sample({ clock: searchTriggered, // когда сработает searchTriggered source: $searchQuery, // возьми данные из $searchQuery filter: $isSearchEnabled, // если поиск активен то продолжаем fn: (query) => { // имитируем поиск return ["result1", "result2"].filter((item) => item.includes(query)); }, target: $searchResults, // обнови $searchResults возвращаемым значением из fn});
Заметьте, что при передаче стора в target
его предыдущее значение будет полностью заменено на возвращаемое значение из fn
.
Обновление от нескольких событий
Стор не ограничен одной подпиской на событие, можно подписаться на сколько угодно событий, а также подписываться на одно и то же событие разными сторами:
import { createEvent, createStore } from "effector";
const $lastUsedFilter = createStore<string | null>(null);const $filters = createStore({ category: "all", searchQuery: "",});
const categoryChanged = createEvent<string>();const searchQueryChanged = createEvent<string>();
// подписываемся двумя разными сторами на одно и то же событие$lastUsedFilter.on(categoryChanged, (_, category) => category);
sample({ clock: categoryChanged, source: $filters, fn: (filters, category) => ({ // придерживаемся принципа иммутабельности ...filters, category, }), // результат fn заменит предыдущее значение в $filters target: $filters,});
// а также подписываемся стором на два события searchQueryChanged и categoryChangedsample({ clock: searchQueryChanged, source: $filters, fn: (filters, searchQuery) => ({ // придерживаемся принципа иммутабельности ...filters, searchQuery, }), // результат fn заменит предыдущее значение в $filters target: $filters,});
Мы подписались двумя сторами на одно и то же событие categoryChanged
, а также стором $filters
на еще одно событие searchQueryChanged
.
Производные сторы
Производный стор вычисляется на основе других сторов и автоматически обновляется при изменении этих сторов, представьте, что мы имеем вот такой стор:
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 }, ],});
И мы хотим отобразить общее количество лайков, а также количество музыки для этого автора. Конечно мы могли бы просто в UI использовать этот стор с помощью хука useUnit
и там уже высчитать эти значения, но это не очень правильно, поскольку мы будем описывать логику в компоненте и размазываем ее по всему приложению, это усложнит поддержку кода в будущем, а если мы захотим использовать эти данные в другом месте, то и вовсе придется дублировать код.
При такой логике правильным подходом будет создать производные сторы на основе $author
используя метод combine
:
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 }, ],});
// общее количество песенconst $totalSongsCount = combine($author, (author) => author.songs.length);// общее количество лайковconst $totalLikesCount = combine($author, (author) => author.songs.reduce((acc, song) => acc + song.likes, 0),);
Каждый из производных сторов будет автоматически обновляться при изменении исходного стора $author
.
Производные сторы обновляются автоматически при изменении исходных сторов, их нельзя передать в target
у sample
или подписаться на событие через .on
.
При этом исходных сторов может быть сколько угодно, что позволяет, например, вычислить текущее состояния приложения:
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
Если вы попробуете использовать значение стора как undefined
или положите в стор это значение:
const $store = createStore(0).on(event, (_, newValue) => { if (newValue % 2 === 0) { return undefined; }
return newValue;});
то столкнетесь с ошибкой в консоли:
store: undefined is used to skip updates. To allow undefined as a value provide explicit { skipVoid: false } option
По умолчанию возвращение undefined
служит как команда “ничего не произошло, пропусти это обновление”. Если вам действительно нужно использовать undefined
как валидное значение, тогда необходимо явно указать это с помощью параметра skipVoid: false
при создании стора:
import { createStore } from "effector";
const $store = createStore(0, { skipVoid: false,});
В ближайших версиях это поведение будет изменено, как показала практика, лучше просто вернуть предыдущее значение для стора, чтобы его не обновлять.
Связанные API и статьи
- API
createStore
- Метод для создания стораStore
- Описание стора и его методов
- Статьи
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.