Управление состоянием
Состояние в 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;
});
Создание стора
import { createStore } from "effector";
// Создание стора с начальным значением
const $counter = createStore(0);
// с явной типизацией
const $user = createStore<{ name: "Bob"; age: 25 } | null>(null);
const $posts = createStore<Post[]>([]);
В effector принято использовать префикс $ для сторов. Это улучшает ориентацию в коде и опыт поиска в вашей IDE.
Чтение значений
Получить текущее значение стора можно несколькими способами:
- При помощи интеграцией фреймворков и хука
useUnit
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
Обновление состояния
Обновление через события
Самый простой и верный способ обновить стор - это привязать его к событию:
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 - сбросили состояние
Обновление с параметрами
Обновить стор можно и с помощью параметров события, достаточно лишь передать данные в событие, как у обычной функции, и использовать в обработчике.
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
для обновления состояния:
- Доступ к актуальным значениям всех сторов
- Атомарность обновлений нескольких сторов
- Удобная трансформация данных через функцию
fn
- Возможность фильтрации обновлений через
filter
- Контроль момента обновления через
clock
Создание стора при помощи метода 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
, а дефолтное значение стора должно соответствовать возвращаемому значению:
import { restore, createEffect } from "effector";
type User = {
id: number;
name: string;
age: number;
};
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(); // 1
reset(); // 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(); // 1
reset(); // 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]);
Познакомиться с полным API для сторов тут
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.