Лучшие практики в 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;
}
}),
);

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

Чтобы лучше контролировать жизненный цикл вашего приложения мы рекомендуем создавать явные событие, например appStarted. Если вам необходим более гранулярный контроль, то не стесняйтесь создавать больше дополнительных событий. Более полную информацию вы найдете на странице Явный запуск приложения.

export const appStarted = createEvent();

Используйте 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
  9. Не создавайте юниты в рантайме
Перевод поддерживается сообществом

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

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

Соавторы