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

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

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

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

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

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

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

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
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

Обновление состояния

Обновление через события

Самый простой и верный способ обновить стор - это привязать его к событию:

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, как использовать этот метод и подробное его описание вы можете познакомиться здесь.

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

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

Производные сторы

Часто нужно создать стор, значение которого зависит от других состояний. Для этого используется метод 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. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.

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

Соавторы