Лучшие практики в Effector

В этом разделе собраны рекомендации по эффективной работе с Effector, основанные на опыте сообщества и команды разработчиков.

Создавайте маленькие сторы

В отличие от Redux, в Effector рекомендуется делать сторы максимально атомарными. Давайте разберем, почему это важно и какие преимущества это дает.

Большие сторы с множеством полей создают несколько проблем:

  • Лишние ре-рендеры: При изменении любого поля обновляются все компоненты, подписанные на стор
  • Тяжелые вычисления: Каждое обновление требует копирования всего объекта
  • Лишние вычисления: если вы имеете производные сторы зависящие от большого стора, то они будут перевычисляться

Атомарные сторы позволяют:

  • Обновлять только то, что действительно изменилось
  • Подписываться только на нужные данные
  • Эффективнее работать с реактивными зависимостями
// ❌ Большой стор - любое изменение вызывает обновление всего
const $bigStore = createStore({
  profile: { /* много полей */ },
  settings: { /* много полей */ },
  posts: [ /* много постов */ ]
})

// ✅ Атомарные сторы - точечные обновления
const $userName = createStore('')
const $userEmail = createStore('')
const $posts = createStore<Post[]>([])
const $settings = createStore<Settings>({})

// Компонент подписывается только на нужные данные
const UserName = () => {
  const name = useUnit($userName) // Обновляется только при изменении имени
  return <h1>{name}</h1>
}

Правила атомарных сторов:

  • Один стор = одна ответственность
  • Стор должен быть неделимым
  • Сторы можно объединять через combine
  • Обновление стора не должно затрагивать другие данные

Immer для сложных объектов

Если ваш стор содержит в себе вложенные структуры, то вы можете использовать всеми любимый Immer для упрощенного обновления:

import { createStore } from "effector";
import { produce } from "immer";

const $users = createStore<User[]>([]);

$users.on(userUpdated, (users, updatedUser) =>
  produce(users, (draft) => {
    const user = draft.find((u) => u.id === updatedUser.id);
    if (user) {
      user.profile.settings.theme = updatedUser.profile.settings.theme;
    }
  }),
);

Явный старт приложения

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

Почему это важно:

  1. Контроль жизненного цикла приложения
  2. Возможность корректного тестирования
  3. Предсказуемое поведение приложения
  4. Возможность явного запуска инициализации
export const appStarted = createEvent();

а также подписаться и запустить событие:

import { sample } from "effector";
import { scope } from "./app.js";

sample({
  clock: appStarted,
  target: initFx,
});

appStarted();

Используйте scope

Команда effector рекомендует всегда использовать Scope, даже если ваше приложение не использует SSR. Это необходимо, чтобы в будущем вы могли спокойно мигрировать на режим работы со Scope.

Хук useUnit

Использование хука useUnit является рекомендуемым способом для работы с юнитами при использовании фреймворков (📘React, 📗Vue и 📘Solid). Почему нужно использовать useUnit:

  • Корректная работа со сторами
  • Оптимизированные обновления
  • Автоматическая работа со Scope – юниты сами знают в каком скоупе они были вызваны

Чистые функции

Используйте чистые функции везде, кроме эффектов, для обработки данных, это обеспечивает:

  • Детерминированный результат
  • Отсутствие сайд-эффектов
  • Проще для тестирования
  • Легче поддерживать
Эта работа для эффектов

Если ваш код может выбросить ошибку или может закончится успехом/неуспехом - то это отличное место для эффектов.

Отладка

Мы настоятельно рекомендуем вам использовать библиотеку patronum и метод debug.

import { createStore, createEvent, createEffect } from "effector";
import { debug } from "patronum/debug";

const event = createEvent();
const effect = createEffect().use((payload) => Promise.resolve("result" + payload));
const $store = createStore(0)
  .on(event, (state, value) => state + value)
  .on(effect.done, (state) => state * 10);

debug($store, event, effect);

event(5);
effect("demo");

// => [store] $store 1
// => [event] event 5
// => [store] $store 6
// => [effect] effect demo
// => [effect] effect.done {"params":"demo", "result": "resultdemo"}
// => [store] $store 60

Однако вам никто не запрещает использовать .watch или createWatch для отладки.

Фабрики

Создание фабрик это частый паттерн при работе с effector, он облегчает использование однотипного кода. Однако вы можете столкнуться с проблемой одинаковых sid, которые могу помешать при работе с SSR.

Чтобы избежать этой проблемы, мы рекомендуем использовать библиотеку @withease/factories.

Если если ваша среда не позволяет добавлять дополнительные зависимости, то вы можете создать свою собственную фабрику следуя этим указаниями.

Работа с сетью

Для удобной работы effector с запросами по сети вы можете использовать farfetched.

Farfetched предоставляет:

  • Мутации и квери
  • Готовое апи для кеширование и др.
  • Независимость от фреймворков

Утилиты для работы с effector

В экосистеме Effector находится библиотека patronum, которая предоставляет готовые решения для работы с юнитами:

  • Управление состоянием (condition, status и др.)
  • Работа со временем (debounce, interval и др.)
  • Функции предикаты (not, or, once и др.)

Упрощение сложной логики с createAction

effector-action - это библиотека, которая позволяет писать императивный код для сложной условной логики, сохраняя при этом декларативную природу effector. При этом effector-action помогает сделать ваш код более читабельным:

import { sample } from "effector";

sample({
  clock: formSubmitted,
  source: {
    form: $form,
    settings: $settings,
    user: $user,
  },
  filter: ({ form }) => form.isValid,
  fn: ({ form, settings, user }) => ({
    data: form,
    theme: settings.theme,
  }),
  target: submitFormFx,
});

sample({
  clock: formSubmitted,
  source: $form,
  filter: (form) => !form.isValid,
  target: showErrorMessageFx,
});

sample({
  clock: submitFormFx.done,
  source: $settings,
  filter: (settings) => settings.sendNotifications,
  target: sendNotificationFx,
});

Именование

Используйте принятые соглашения об именовании:

  • Для хранилищ – префикс $
  • Для эффектов – постфикс fx, это позволит вам отличать ваши эффекты от событий
  • Для событий – правил нет, однако мы предлагаем вам называть события, которые напрямую запускают обновления сторов, как будто они уже произошли.
const updateUserNameFx = createEffect(() => {});

const userNameUpdated = createEvent();

const $userName = createStore("JS");

$userName.on(userNameUpdated, (_, newName) => newName);

userNameUpdated("TS");
Соглашение об именовании

Выбор между префиксом или постфиксом в основном является вопросом личных предпочтений. Это необходимо для улучшения опыта поиска в вашей IDE.

Антипаттерны

Использование watch для логики

watch следует использовать только для отладки.

// Логика в watch
$user.watch((user) => {
  localStorage.setItem("user", JSON.stringify(user));
  api.trackUserUpdate(user);
  someEvent(user.id);
});

Сложные вложенные sample

Избегайте сложных и вложенных цепочек sample.

Абстрактные названия в колбеках

Используйте осмысленные имена вместо абстрактных value, data, item.

$users.on(userAdded, (state, payload) => [...state, payload]);

sample({
  clock: buttonClicked,
  source: $data,
  fn: (data) => data,
  target: someFx,
});

Императивные вызовы в эффектах

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

const loginFx = createEffect(async (params) => {
  const user = await api.login(params);

  // Императивные вызовы
  setUser(user);
  redirectFx("/dashboard");
  showNotification("Welcome!");

  return user;
});

Использование getState

Не используйте $store.getState для получения значений. Если вам нужно получить данные какого-то стора, то передайте его туда, например в source у sample:

const submitFormFx = createEffect((formData) => {
  // Получаем значения через getState
  const user = $user.getState();
  const settings = $settings.getState();

  return api.submit({
    ...formData,
    userId: user.id,
    theme: settings.theme,
  });
});

Бизнес-логика в UI

Не тащите вашу логику в UI элементы, это основная философия effector и то, от чего effector пытается избавить вас, а именно зависимость логики от UI.

Кратко об антипаттернах:

  1. Не используйте watch для логики, только для отладки
  2. Избегайте прямых мутаций в сторах
  3. Не создавайте сложные вложенные sample, их сложно читать
  4. Не используйте большие сторы, используйте атомарный подход
  5. Используйте осмысленные названия параметров, а не абстрактные
  6. Не вызывайте события внутри эффектов императивно
  7. Не используйте $store.getState для работы
  8. Не тащите логику в UI
Перевод поддерживается сообществом

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

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

Соавторы