Лучшие практики в 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));
// Связываем через sample
sample({
clock: $user,
target: [saveToStorageFx, trackUpdateFx],
});
// Для событий тоже используем sample
sample({
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));
// Связываем через sample
sample({
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 }) => {});
// Получаем все необходимые данные через sample
sample({
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. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.