Лучшие практики в 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; } }),);
Явный старт приложения
Мы рекомендуем использовать явный старт приложения через специальные события, чтобы запустить инициализацию.
Почему это важно:
- Контроль жизненного цикла приложения
- Возможность корректного тестирования
- Предсказуемое поведение приложения
- Возможность явного запуска инициализации
export const appStarted = createEvent();
а также подписаться и запустить событие:
import { sample } from "effector";import { scope } from "./app.js";
sample({ clock: appStarted, target: initFx,});
appStarted();
import { sample, allSettled } from "effector";import { scope } from "./app.js";
sample({ clock: appStarted, target: initFx,});
allSettled(appStarted, { scope });
Используйте 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,});
import { createAction } from "effector-action";
const submitForm = createAction({ source: { form: $form, settings: $settings, user: $user, }, target: { submitFormFx, showErrorMessageFx, sendNotificationFx, }, fn: (target, { form, settings, user }) => { if (!form.isValid) { target.showErrorMessageFx(form.errors); return; }
target.submitFormFx({ data: form, theme: settings.theme, }); },});
createAction(submitFormFx.done, { source: $settings, target: sendNotificationFx, fn: (sendNotification, settings) => { if (settings.sendNotifications) { sendNotification(); } },});
submitForm();
Именование
Используйте принятые соглашения об именовании:
- Для сторов – префикс
$
- Для эффектов – постфикс
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);});
// Отдельные эффекты для сайд-эффектовconst saveToStorageFx = createEffect((user: User) => localStorage.setItem("user", JSON.stringify(user)),);
const trackUpdateFx = createEffect((user: User) => api.trackUserUpdate(user));
// Связываем через samplesample({ clock: $user, target: [saveToStorageFx, trackUpdateFx],});
// Для событий тоже используем samplesample({ clock: $user, fn: (user) => user.id, target: someEvent,});
Сложные вложенные sample
Избегайте сложных и вложенных цепочек sample
.
Абстрактные названия в колбеках
Используйте осмысленные имена вместо абстрактных value
, data
, item
.
$users.on(userAdded, (state, payload) => [...state, payload]);
sample({ clock: buttonClicked, source: $data, fn: (data) => data, target: someFx,});
$users.on(userAdded, (users, newUser) => [...users, newUser]);
sample({ clock: buttonClicked, source: $userData, fn: (userData) => userData, target: updateUserFx,});
Императивные вызовы в эффектах
Не вызывайте события или эффекты императивно внутри других эффектов, вместо этого используйте декларативный стиль.
const loginFx = createEffect(async (params) => { const user = await api.login(params);
// Императивные вызовы setUser(user); redirectFx("/dashboard"); showNotification("Welcome!");
return user;});
const loginFx = createEffect((params) => api.login(params));// Связываем через samplesample({ clock: loginFx.doneData, target: [ $user, // Обновляем стор redirectToDashboardFx, showWelcomeNotificationFx, ],});
Использование 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, });});
// Получаем значения через параметрыconst submitFormFx = createEffect(({ form, userId, theme }) => {});
// Получаем все необходимые данные через samplesample({ clock: formSubmitted, source: { form: $form, user: $user, settings: $settings, }, fn: ({ form, user, settings }) => ({ form, userId: user.id, theme: settings.theme, }), target: submitFormFx,});
Бизнес-логика в UI
Не тащите вашу логику в UI элементы, это основная философия effector и то, от чего effector пытается избавить вас, а именно зависимость логики от UI.
Кратко об антипаттернах:
- Не используйте
watch
для логики, только для отладки - Избегайте прямых мутаций в сторах
- Не создавайте сложные вложенные
sample
, их сложно читать - Не используйте большие сторы, используйте атомарный подход
- Используйте осмысленные названия параметров, а не абстрактные
- Не вызывайте события внутри эффектов императивно
- Не используйте
$store.getState
для работы - Не тащите логику в UI
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.