Управление состоянием

Состояние в effector управляется через сторы (Store) - специальные объекты, которые хранят значения и обновляют их при получении событий. Сторы создаются с помощью функции createStore.

Иммутабельность данных

Данные сторов в effector иммутабельные – это значит, что вы не должны мутировать массивы или объекты напрямую, а создавать новые инстансы при обновлениях.

// обновление массива данных
$users.on(userAdded, (users, newUser) => [...users, newUser]);

//обновление объекта
$user.on(nameChanged, (user, newName) => ({
  ...user,
  name: newName,
}));

Создание стора

Создать новый стор можно при помощи метода createStore:

import { createStore } from "effector";

// Создание стора с начальным значением
const $counter = createStore(0);
// с явной типизацией
const $user = createStore<{ name: "Bob"; age: 25 } | null>(null);
const $posts = createStore<Post[]>([]);
Наименование сторов

В effector принято использовать префикс $ для сторов. Это улучшает ориентацию в коде и опыт поиска в вашей IDE.

Чтение значений

Получить текущее значение стора можно несколькими способами:

  1. При помощи интеграцией фреймворков и хука useUnit (📘 React, 📗 Vue, 📘 Solid):
import { useUnit } from 'effector-react'
import { $counter } from './model.js'

const Counter = () => {
  const counter = useUnit($counter)

  return <div>{counter}</div>
}
  1. Подписка на изменения через watch - только для дебага или интеграций
$counter.watch((counter) => {
  console.log("Counter changed:", counter);
});
  1. Метод 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, как использовать этот метод и подробное его описание вы можете познакомиться здесь.

Преимущества sample для обновления состояния:

  1. Доступ к актуальным значениям всех сторов
  2. Атомарность обновлений нескольких сторов
  3. Контроль момента обновления через clock
  4. Возможность фильтрации обновлений через filter
  5. Удобная трансформация данных через функцию 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(); // 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]);

Значения 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

Вы можете использовать null вместо undefined для отсутствующих значений.

Познакомиться с полным API для сторов тут

Перевод поддерживается сообществом

Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.

Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.

Соавторы