Сайд-эффекты и асинхронность
Для работы с сайд-эффектами и любой асинхронностью effector предоставляет эффекты. Сайд-эффектами является все, что может повлиять на чистоту вашей функции, например:
- HTTP запрос на сервер
- Изменение или работа с глобальными переменными
- Взаимодействие с браузерным API (
addEventListener,setTimeoutи тд) - Работа с
localStorage,IndexedDBи другими хранилищами - Любой код, который может выбросить ошибку, или выполняться какое-то время
import { createEffect, sample, createStore } from "effector";
const $user = createStore(null);
const fetchUserFx = createEffect(async (userId: string) => { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { throw new Error("Failed to fetch user"); } return response.json();});
// при успешном выполнении эффекта $user будет обновлен возвращаемым значениемsample({ clock: fetchUserFx.doneData, target: $user,});Однако зачем нам вообще эффекты? В effector большинство функций, например store.on, sample.fn и другие являются чистыми, то есть работают только с данными полученными через аргументы. Такие функции не могут быть асинхронными или иметь сайд-эффектов, так как это нарушит предсказуемость и реактивность.
Преимущества эффектов
Эффект является контейнером для сайд-эффектов или асинхронных функций, которые могут либо выкинуть ошибку во время выполнения, либо же выполняться неопределенное время, и чтобы связать эффекты с реактивной системой у них есть удобные свойства. Вот несколько из них:
pending— стор, который указывает выполняется ли эффект, удобно чтобы показывать лоадер в UI.doneData— событие, срабатывает когда эффект завершился без ошибок.failData— еще одно событие, которое сработает если во время выполнения эффекта выбросилась ошибка с результатом ошибки.
Любое из свойств эффектов вызывается само ядром effector, это значит, что вам не нужно самим пытаться вызвать их вручную.
Поскольку у эффектов есть свои события, то работа с ними происходит также, как и при работе с обычными событиями, однако давайте посмотрим на простой пример использования эффектов:
import { createEffect, createEvent, createStore, sample } from "effector";
export const $error = createStore<string | null>(null);
export const submit = createEvent();
// простая отправка формы, но обертнутая в эффектconst sendFormFx = createEffect(async ({ name, email }: { name: string; email: string }) => { try { await fetch("/api/user-data", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name, email }), }); } catch { throw new Error("Failed to send form"); }});
export const $isLoading = sendFormFx.pending;
sample({ clock: sendFormFx.failData, fn: (error) => error.message, target: $error,});
sample({ clock: submit, target: sendFormFx,});Эффекты, как и события, способны принимать только один аргумент. Если вам необходимо передать несколько аргументов, то используйте объект, как в примере выше { name, email }
В UI мы просто будем вызывать submit событие, отображать лоадер во время загрузки и показывать ошибку, если таковая будет:
import { useUnit } from "effector-react";import { $error, $isLoading, submit } from "./model.ts";
const Form = () => { const { isLoading, error } = useUnit({ isLoading: $isLoading, error: $error, }); const onSubmit = useUnit(submit);
return ( <form onSubmit={onSubmit}> <input name="name" /> <input name="email" /> <button type="submit">Submit</button> {isLoading && <div>Loading...</div>} {error && <div>{error}</div>} </form> );};Рассмотрим чуть подробнее пример модели, в примере ниже мы связываем событие failData эффекта и стора $error, событие failData передаст значение ошибки в стор $error:
sample({ clock: sendFormFx.failData, fn: (error) => error.message, target: $error,});А при вызове события submit просто вызвать эффект положив его в target:
sample({ clock: submit, target: sendFormFx,});Вызовы эффектов
Вызовы эффектов аналогичны вызову событий, вы можете вызывать его внутри компонента, обернув его перед этим в хук useUnit и использовав возвращаемое значение как функцию:
const fetchUser = useUnit(fetchUserFx);Однако такой способ не рекомендуется, чтобы наш UI слой не знал много о бизнес-логике. Альтернатива это создать событие, которое мы будем экспортировать, для запуске этого эффекта в модели и связать их с помощью sample, при этом аргументы с которыми было вызвано событие будут переданы в эффект:
import { createEvent, sample } from "effector";
export const updateProfileButtonPressed = createEvent<string>();
sample({ clock: updateProfileButtonPressed, target: fetchUserFx,});На странице Как мыслить в парадигме effector мы рассказали, почему нужно разделять бизнес-логику и UI.
Вы также можете вызывать эффекты внутри самих эффектов:
import { createEffect } from "effector";
const fetchInitialData = createEffect(async (userId: string) => { const userData = await getUserByIdFx(userId);
const friends = await getUserByIds(userData.friends);
return userData.name;});Вызов событий в эффекте
Вы можете вызывать события внутри эффекта, например это может быть полезно когда мы хотим запустить событие по таймеру:
import { createEffect, createEvent } from "effector";
const tick = createEvent();
const fetchInitialData = createEffect(async () => { //не забудьте в будущем очистить id! const id = setInterval(() => { tick(); }, 1000);});Однако здесь может возникнуть Потеря скоупа если вы работаете со скоупом, в этом случае вам нужно использовать scopeBind. Если вы не используете скоуп, то ничего оборачивать не нужно.
Обновить стор с помощью эффекта
Классический пример использования эффектов – мы хотим обновить стор по его завершению. Логика работы здесь такая же, как и при работе с событиями, просто подписываемся на doneData и передаем нужный стор в target:
import { createStore, createEffect } from "effector";
const fetchUserNameFx = createEffect(async (userId: string) => { const userData = await fetch(`/api/users/${userId}`);
return userData.name;});
const $error = createStore<string | null>(null);const $userName = createStore("");const $isLoading = fetchUserNameFx.pending;
sample({ clock: fetchUserNameFx.doneData, target: $userName,});
sample({ clock: fetchUserNameFx.failData, fn: (error) => error.message, target: $error,});Обработка ошибок
Эффект способен отловить, когда во время его выполнения происходит ошибка и передать ее в событие failData. И порой мы хотим выкинуть свою ошибку, а не обычный Error:
import { createEffect } from "effector";
class CustomError extends Error { // реализация}
const effect = createEffect(async () => { const response = await fetch(`/api/users/${userId}`);
if (!response.ok) { // Вы можете выбрасывать ошибки, которые будут перехвачены обработчиком .failData throw new CustomError(`Не удалось загрузить пользователя: ${response.statusText}`); }
return response.json();});Код выше абсолютно вылидный и отработает как надо, однако если мы перехватим ошибку с помощью события failData, то заметим тип Error вместо нашей кастомной ошибки CustomError:
sample({ clock: effect.failData, // error будет типа Error, а не CustomError fn: (error) => error.message, target: $error,});Вся проблема кроется в типах, по умолчанию эффект ожидает, что выкинет тип ошибки Error и конечно не может знать, что у нас в теле функции выкидывается CustomError. Решение проблемы простое, нужно передать нужный тип в дженерик createEffect, однако также придется и прокинуть тип принимаемых параметров и возвращаемое значение:
import { createEffect } from "effector";
class CustomError extends Error { // реализация}
const effect = createEffect<Params, Done, CustomError>(async () => { const response = await fetch(`/api/users/${userId}`);
if (!response.ok) { // Вы можете выбрасывать ошибки, которые будут перехвачены обработчиком .failData throw new CustomError(`Не удалось загрузить пользователя: ${response.statusText}`); }
return response.json();});Более подробнее о типизации эффектов и других юнитов можно прочитать на странице Типизация.
Переиспользование эффектов
Нередкий кейс, это когда у нас имеется общий эффект, например fetchShopCardsFx, который мы можем переиспользовать в нескольких местах приложения и при этом мы все также хотим подписаться на его события doneData, failData или любое другое, однако если мы это сделаем, то столкнемся с проблемным поведением, когда на одной странице будет вызываться этот эффект, а его подписчики будут тригириться везде, потому что мы использовали один и тот же общий эффект. И это нормальное поведение, потому что юниты эффектора должны объявляться статически на уровне модуля, но все же не то, которое бы мы хотели. Решением проблемы будет использовать оператор attach, который создаст копию прикрепленного эффекта, а с этой копией мы можем уже работать только на нашей странице:
import { createEffect, attach, createEvent } from "effector";
const showNotification = createEvent();
// где-то в общем переипользуемом местеconst fetchShopCardsFx = createEffect(async () => { const response = await fetch("/api/shop-cards"); return response.json();});
// наша локальная копия, на которую можем смело подписатьсяconst fetchShopCardsAttachedFx = attach({ effect: fetchShopCards,});
sample({ clock: fetchShopCardsAttachedFx.failData, target: showNotification,});При вызове прикрепленного эффекта, созданного с помощью attach,также будет вызываться и оригинальный эффект, переданный в effect.
Связанные API и статьи
- API
Effect- Описание эффекта и его методовcreateEffect- Метод для создания эффектовattach- Метод, который позволяет создавать новые эффекты на основе существующих
- Статьи
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.