Управление состоянием
Состояние в effector управляется через сторы (Store) - специальные объекты, которые хранят значения и обновляют их при получении событий. Сторы создаются с помощью функции createStore.
Данные сторов в effector иммутабельные – это значит, что вы не должны мутировать массивы или объекты напрямую, а создавать новые инстансы при обновлениях.
// обновление массива данных$users.on(userAdded, (users, newUser) => [...users, newUser]);
//обновление объекта$user.on(nameChanged, (user, newName) => ({ ...user, name: newName,}));
// обновление массива данных$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.
Чтение значений
Получить текущее значение стора можно несколькими способами:
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>}
- Подписка на изменения через
watch
- только для дебага или интеграций
$counter.watch((counter) => { console.log("Counter changed:", counter);});
- Метод
getState()
- только для интеграций
console.log($counter.getState()); // 0
Обновление состояния
В effector обновление состояния происходит через события. Вы можете изменить состояние подписавшись на событие через .on
или при помощи метода sample
.
Состояние стора обновляется когда получает значение, которое не равно (!==) текущему, а также не равно undefined
.
Обновление через события
Самый простой и верный способ обновить стор - это привязать его к событию:
import { createStore, createEvent } from "effector";
const incremented = createEvent();const decremented = createEvent();const resetCounter = createEvent();
const $counter = createStore(0) // Увеличиваем значение на 1 при каждом вызове события .on(incremented, (counterValue) => counterValue + 1) // Уменьшаем значение на 1 при каждом вызове события .on(decremented, (counterValue) => counterValue - 1) // Сбрасываем значение в 0 .reset(resetCounter);
$counter.watch((counterValue) => console.log(counterValue));
// Использованиеincremented();incremented();decremented();
resetCounter();
// Вывод в консоль// 0 - вывод при инициализации// 1// 2// 1// 0 - сбросили состояние
Если вы не знакомы с createEvent
и событиями, то вы узнаете как работать с ними на следующей странице.
Обновление с параметрами
Обновить стор можно и с помощью параметров события, достаточно лишь передать данные в событие, как у обычной функции, и использовать в обработчике:
import { createStore, createEvent } from "effector";
const userUpdated = createEvent<{ name: string }>();
const $user = createStore({ name: "Bob" });
$user.on(userUpdated, (user, changedUser) => ({ ...user, ...changedUser,}));
userUpdated({ name: "Alice" });
Сложная логика обновления
При помощи метода on
мы можем обновить состояние стора для простых случаев, при срабатывании какого-то события, передав данные из события либо обновить на основе предыдущего значения.
Однако это не всегда покрывает все нужды. Для более сложной логики обновления состояния мы можем воспользоваться методом sample
, который помогает нам в случае когда:
- Нужно контролировать обновление стора при помощи события
- Требуется обновить стор на основе значений других сторов
- Нужна трансформация данных перед обновлением стора с доступом к актуальным значениям других сторов
Например:
import { createEvent, createStore, sample } from "effector";
const updateItems = createEvent();
const $items = createStore([1, 2, 3]);const $filteredItems = createStore([]);const $filter = createStore("even");
// sample автоматически предоставляет доступ к актуальным значениям// всех связанных сторов в момент срабатывания событияsample({ clock: updateItems, source: { items: $items, filter: $filter }, fn: ({ items, filter }) => { if (filter === "even") { return items.filter((n) => n % 2 === 0); }
return items.filter((n) => n % 2 === 1); }, target: $filteredItems,});
О том, что такое sample
, как использовать этот метод и подробное его описание вы можете познакомиться здесь.
Преимущества sample
для обновления состояния:
- Доступ к актуальным значениям всех сторов
- Атомарность обновлений нескольких сторов
- Контроль момента обновления через
clock
- Возможность фильтрации обновлений через
filter
- Удобная трансформация данных через функцию
fn
Создание стора при помощи метода restore
Если у вас работа со стором подразумевает замену старого состояние на новое при вызове события, то вы можете использовать метод restore
:
import { restore, createEvent } from "effector";
const nameChanged = createEvent<string>();
const $counter = restore(nameChanged, "");
Код выше эквивалентен коду ниже:
import { createStore, createEvent } from "effector";
const nameChanged = createEvent<string>();
const $counter = createStore("").on(nameChanged, (_, newName) => newName);
Также метод restore
можно использовать и с эффектом, в таком случае в стор попадут данные из события эффекта doneData
, а дефолтное значение стора должно соответствовать возвращаемому значению:
Если вы не знакомы с createEffect
и эффектами, то вы узнаете как работать с ними на этой странице.
import { restore, createEffect } from "effector";
// упустим реализацию типовconst createUserFx = createEffect<string, User>((id) => { return { id: 4, name: "Bob", age: 18, };});
const $newUser = restore(createUserFx, { id: 0, name: "", age: -1,});
createUserFx();
// После успешного завершения работы эффекта// $newUser будет:// {// id: 4,// name: "Bob",// age: 18,// }
Множественные обновления
Стор не ограничен одной подпиской на событие, вы можете подписаться на столько событий, сколько вам нужно, а также подписываться на одно и то же событие разными сторами:
import { createEvent, createStore } from "effector";
const categoryChanged = createEvent<string>();const searchQueryChanged = createEvent<string>();const filtersReset = createEvent();
const $lastUsedFilter = createStore<string | null>(null);const $filters = createStore({ category: "all", searchQuery: "",});
// подписываемся двумя разными сторами на одно и то же событие$lastUsedFilter.on(categoryChanged, (_, categoty) => category);$filters.on(categoryChanged, (filters, category) => ({ ...filters, category,}));
$filters.on(searchQueryChanged, (filters, searchQuery) => ({ ...filters, searchQuery,}));
$filters.reset(filtersReset);
В этом примере мы подписываемся стором $filters
на несколько событий, а также двумя сторами $filters
и $lastUsedFilter
на одно и то же событие categoryChanged
.
Упрощение обновлений с createApi
Когда вам нужно создать множество обработчиков для одного стора, вместо создания отдельных событий и подписки на них, вы можете использовать createApi
. Эта функция создает набор событий для обновления стора в одном месте.
Следующие примеры кода эквиваленты:
import { createStore, createApi } from "effector";
const $counter = createStore(0);
const { increment, decrement, reset } = createApi($counter, { increment: (state) => state + 1, decrement: (state) => state - 1, reset: () => 0,});
// Использованиеincrement(); // 1reset(); // 0
import { createStore, createEvent } from "effector";
const $counter = createStore(0);
const incrementClicked = createEvent();const decrementClicked = createEvent();const resetClicked = createEvent();
$counter .on(incrementClicked, (state) => state + 1) .on(decrementClicked, (state) => state - 1) .reset(resetClicked);
// Использованиеincrement(); // 1reset(); // 0
Производные сторы
Часто нужно создать стор, значение которого зависит от других состояний. Для этого используется метод map
:
import { createStore, combine } from "effector";
const $currentUser = createStore({ id: 1, name: "Winnie Pooh",});const $users = createStore<User[]>([]);
// Отфильтрованный списокconst $activeUsers = $users.map((users) => users.filter((user) => user.active));
// Вычисляемое значениеconst $totalUsersCount = $users.map((users) => users.length);const $activeUsersCount = $activeUsers.map((users) => users.length);
// Комбинация нескольких сторовconst $friendsList = combine($users, $currentUser, (users, currentUser) => users.filter((user) => user.friendIds.includes(currentUser.id)),);
Мы также использовали здесь метод combine
, который позволяет нам объединить значение нескольких сторов в одно.
Также можно комбинировать сторы в объект:
import { combine } from "effector";
const $form = combine({ name: $name, age: $age, city: $city,});
// или с дополнительной трансформациейconst $formValidation = combine($name, $age, (name, age) => ({ isValid: name.length > 0 && age >= 18, errors: { name: name.length === 0 ? "Required" : null, age: age < 18 ? "Must be 18+" : null, },}));
Производные сторы обновляются автоматически при изменении исходных сторов. Не нужно вручную синхронизировать их значения.
Сброс состояния
Вы можете сбросить состояние стора до исходного при помощи метода reset
:
const formSubmitted = createEvent();const formReset = createEvent();
const $form = createStore({ email: "", password: "" }) // очищаем форму при сабмите и явном сбросе .reset(formReset, formSubmitted); // или .reset([formReset, formSubmitted]);
Значения undefined
По умолчанию effector пропускает обновления со значением undefined
. Это сделано для того, чтобы можно было ничего не возвращать из редьюсеров, если обновление стора не требуется:
const $store = createStore(0).on(event, (_, newValue) => { if (newValue % 2 === 0) { return; }
return newValue;});
Это поведение будет отключено в будущем! Как показала практика, будет лучше просто возвращать предыдущее значение стора.
Если вам нужно использовать undefined
как валидное значение, необходимо явно указать с помощью skipVoid: false
при создании стора:
import { createStore, createEvent } from "effector";
const setVoidValue = createEvent<number>();
// ❌ undefined будут пропущеныconst $store = createStore(13).on(setVoidValue, (_, voidValue) => voidValue);
// ✅ undefined разрешены как значенияconst $store = createStore(13, { skipVoid: false,}).on(setVoidValue, (_, voidValue) => voidValue);
setVoidValue(null);
Вы можете использовать null
вместо undefined
для отсутствующих значений.
Познакомиться с полным API для сторов тут
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.