Композиция юнитов в effector
В Effector есть два мощных инструмента для связывания юнитов между собой: sample
и attach
. Хотя они могут показаться похожими, у каждого из них есть свои особенности и сценарии использования.
Sample: связь данных и событий
sample
- это универсальный инструмент для связывания юнитов. Его главная задача - брать данные из одного места source
и передавать их в другое место target
при срабатывании определённого триггера clock
.
Общий паттерн работы метода sample
следующий:
- Сработай при вызове тригера
clock
- Возьми данные из
source
- Отфильтруй данные, если все корректно, то верни
true
и иди дальше по цепочке, иначеfalse
- Преобразуй данные при помощи
fn
- Отдай данные в
target
.
Базовое использование sample
import { createStore, createEvent, sample, createEffect } from "effector";
const buttonClicked = createEvent();
const $userName = createStore("Bob");
const fetchUserFx = createEffect((userName) => { // логика});
// При клике на кнопку получаем текущее имяsample({ clock: buttonClicked, source: $userName, target: fetchUserFx,});
Если вы не укажете clock
, то источником вызова также может послужить и source
. Вы должны использовать хотя бы один из этих свойств аргумента!
import { createStore, sample } from "effector";
const $currentUser = createStore({ name: "Bob", age: 25 });
// создает производный стор, который обновляется, когда source меняетсяconst $userAge = sample({ source: $currentUser, fn: (user) => user.age,});// эквивалентноconst $userAgeViaMap = $currentUser.map((currentUser) => currentUser.age);
Как вы можете заметить метод sample
очень гибкий и может использоваться в различных сценариях:
- Когда нужно взять данные из стора в момент события
- Для трансформации данных перед отправкой
- Для условной обработки через filter
- Для синхронизации нескольких источников данных
- Последовательная цепочка запуска юнитов
Фильтрация данных
Вам может потребоваться запустить цепочку вызова, при выполнение каких-то условий, для таких ситуаций метод sample
позволяет фильтровать данные с помощью параметра filter
:
import { createEvent, createStore, sample, createEffect } from "effector";
type UserFormData = { username: string; age: number;};
const submitForm = createEvent();
const $formData = createStore<UserFormData>({ username: "", age: 0 });
const submitToServerFx = createEffect((formData: UserFormData) => { // логика});
sample({ clock: submitForm, source: $formData, filter: (form) => form.age >= 18 && form.username.length > 0, target: submitToServerFx,});
submitForm();
При вызове submitForm
мы берем данные из source, проверем в filter
по условиям, если проверка прошла успешно, то возвращаем true
и вызываем target
, в ином случае false
и ничего больше не делаем.
Функции fn
и filter
должны быть чистыми функциями! Чистая функция - это функция, которая всегда возвращает один и тот же результат для одинаковых входных данных и не производит никаких побочных эффектов (не изменяет данные вне своей области видимости).
Трансформация данных
Часто нужно не просто передать данные, но и преобразовать их. Для этого используется параметр fn
:
import { createEvent, createStore, sample } from "effector";
const buttonClicked = createEvent();const $user = createStore({ name: "Bob", age: 25 });const $userInfo = createStore("");
sample({ clock: buttonClicked, source: $user, fn: (user) => `${user.name} is ${user.age} years old`, target: $userInfo,});
Несколько источников данных
Можно использовать несколько сторов как источник данных:
import { createEvent, createStore, sample, createEffect } from "effector";
type SubmitSearch = { query: string; filters: Array<string>;};
const submitSearchFx = createEffect((params: SubmitSearch) => { /// логика});
const searchClicked = createEvent();
const $searchQuery = createStore("");const $filters = createStore<string[]>([]);
sample({ clock: searchClicked, source: { query: $searchQuery, filters: $filters, }, target: submitSearchFx,});
Несколько источников вызова sample
sample
позволяет использовать массив событий в качестве clock
, что очень удобно когда нам нужно обработать одинаковым образом несколько разных триггеров. Это помогает избежать дублирования кода и делает логику более централизованной.
import { createEvent, createStore, sample } from "effector";
// События для разных действий пользователяconst saveButtonClicked = createEvent();const ctrlSPressed = createEvent();const autoSaveTriggered = createEvent();
// Общее хранилище данныхconst $formData = createStore({ text: "" });
// Эффект сохраненияconst saveDocumentFx = createEffect((data: { text: string }) => { // Логика сохранения});
// Единая точка сохранения документа, которая срабатывает от любого триггераsample({ // Все эти события будут вызывать сохранение clock: [saveButtonClicked, ctrlSPressed, autoSaveTriggered], source: $formData, target: saveDocumentFx,});
Массив target
в sample
sample
позволяет передавать массив юнитов в target
, что полезно когда одни и те же данные нужно направить в несколько мест одновременно. В target
можно передать массив любых юнитов - событий, эффектов или сторов.
import { createEvent, createStore, createEffect, sample } from "effector";
// Создаем юниты куда будут направляться данныеconst userDataReceived = createEvent<User>();const $lastUserData = createStore<User | null>(null);const saveUserFx = createEffect<User, void>((user) => { // Сохраняем пользователя});const logUserFx = createEffect<User, void>((user) => { // Логируем действия с пользователем});
const userUpdated = createEvent<User>();
// При обновлении пользователя:// - Сохраняем данные через saveUserFx// - Отправляем в систему логирования через logUserFx// - Обновляем стор $lastUserData// - Вызываем событие userDataReceivedsample({ clock: userUpdated, target: [saveUserFx, logUserFx, $lastUserData, userDataReceived],});
Важные моменты:
- Все юниты в target должны быть совместимы по типу с данными из
source
/clock
- Порядок выполнения целей гарантирован - они будут вызваны в порядке написания
- Можно комбинировать разные типы юнитов в массиве
target
Возвращаемое значение sample
sample
возвращает юнит, тип которого зависит от конфигурации:
С target
Если указан target
, sample
вернёт этот же target
:
const $store = createStore(0);const submitted = createEvent();const sendData = createEvent<number>();
// result будет иметь тип EventCallable<number>const result = sample({ clock: submitted, source: $store, target: sendData,});
Без target
Когда target
не указан, то тип возвращаемого значения зависит от передаваемых параметров.
Если НЕ указан filter
, а также clock
и source
являются сторами, то результат будет производным стором с типом данных из source
.
import { createStore, sample } from "effector";
const $store = createStore("");const $secondStore = createStore(0);
const $derived = sample({ clock: $secondStore, source: $store,});// $derived будет Store<string>
const $secondDerived = sample({ clock: $secondStore, source: $store, fn: () => false,});// $secondDerived будет Store<boolean>
Если используется fn
, то тип возвращаемого значения будет соответствовать результату функции.
В остальных же случаях возвращаемое значение будет производным событием с типом данных зависящий от source
, которое нельзя вызвать самому, однако можно подписаться на него!
Метод sample
полностью типизирован, и принимает тип в зависимости от передаваемых параметров!
import { createStore, createEvent, sample } from "effector";
const $store = createStore(0);
const submitted = createEvent<string>();
const event = sample({ clock: submitted, source: $store,});// event имеет тип Event<number>
const secondSampleEvent = sample({ clock: submitted, source: $store, fn: () => true,});// Event<true>
Практический пример
Давайте рассмотрим практический пример, когда при выборе id пользователя нам нужно проверить является ли он админом, сохранить выбранного пользователя в сторе, и на основе выбранного id создать производный стор с данными о пользователе
import { createStore, createEvent, sample } from "effector";
type User = { id: number; role: string;};
const userSelected = createEvent<number>();
const $users = createStore<User[]>([]);
// Создаём производный стор, который будет хранить выбранного пользователяconst $selectedUser = sample({ clock: userSelected, source: $users, fn: (users, id) => users.find((user) => user.id === id) || null,});// $selectedUser имеет тип Store<User | null>
// Создаём производное событие, которое будет срабатывать только для админов// если выбранный пользователь админ, то событие сработает сразуconst adminSelected = sample({ clock: userSelected, source: $users, // сработает только если пользователь найден и он админ filter: (users, id) => !!users.find((user) => user.id === id && user.role === "admin"), fn: (users, id) => users.find((user) => user.id === id)!,});// adminSelected имеет тип Event<User>
userSelected(2);
Attach: специализация эффектов
attach
- это инструмент для создания новых эффектов на основе существующих, с доступом к данным из сторов. Это особенно полезно когда нужно:
- Добавить контекст к эффекту
- Переиспользовать логику эффекта с разными параметрами
- Инкапсулировать доступ к стору
import { attach, createEffect, createStore } from "effector";
type SendMessageParams = { text: string; token: string };
// Базовый эффект для отправки данныхconst baseSendMessageFx = createEffect<SendMessageParams, void>(async ({ text, token }) => { await fetch("/api/messages", { method: "POST", headers: { Authorization: `Bearer ${token}`, }, body: JSON.stringify({ text }), });});
// Стор с токеном авторизацииconst $authToken = createStore("default-token");
// Создаём специализированный эффект, который автоматически использует токенconst sendMessageFx = attach({ effect: baseSendMessageFx, source: $authToken, mapParams: (text: string, token) => ({ text, token, }),});
// Теперь можно вызывать эффект только с текстом сообщенияsendMessageFx("Hello!"); // токен будет добавлен автоматически
Очень удобно использовать attach
для переиспользования логики:
const fetchDataFx = createEffect<{ endpoint: string; token: string }, any>();
// Создаём специализированные эффекты для разных эндпоинтовconst fetchUsersFx = attach({ effect: fetchDataFx, mapParams: (_, token) => ({ endpoint: "/users", token, }), source: $authToken,});
const fetchProductsFx = attach({ effect: fetchDataFx, mapParams: (_, token) => ({ endpoint: "/products", token, }), source: $authToken,});
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.