Лучшие практики в 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. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.

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

Соавторы