Композиция юнитов
Если считать, что каждый юнит это кирпичик нашего приложения, тогда для полноценного функционирования нам необходимо как-то склеить эти кирпичики вместе, например при срабатывании события подтверждении формы, провалидировать данные формы, если все корректно вызвать эффект с отправкой данных, а также обновить наш стор, иначе говоря построить связи между юнитами. Чтобы такое реализовать необходимо использовать оператор sample или createAction.
Концептуально оба оператора выполняют одинаковую работу, однако у них есть отличие: sample – декларативный оператор, в то время как createAction – более императивный, который позволяет в более привычном стиле описывать логику работу.
// sampleimport { sample, createEvent } from "effector";
const sendButtonClicked = createEvent();
sample({ clock: sendButtonClicked, source: $formData, filter: (form) => form.username.length > 0 && form.age >= 18, fn: (form) => ({ ...form, timestamp: Date.now(), }), target: [sendFormFx, formSubmitted],});// createActionimport { createAction } from "effector-action";import { createEvent } from "effector";
const sendButtonClicked = createEvent();
createAction(sendButtonClicked, { source: $formData, target: { sendForm: sendFormFx, formSubmitted, }, fn: (target, form) => { if (form.username.length > 0 && form.age >= 18) { const updatedForm = { ...form, timestamp: Date.now(), }; target.sendForm(updatedForm); target.formSubmitted(); } },});Оба оператора срабатывают при вызове события sendButtonClicked, затем берут данные из source, а дальше:
- В
sampleиспользуются отдельные параметры:filterдля проверки условий,fnдля трансформации данных, иtargetдля вызова юнитов. - В
createActionвся логика находится в однойfn, где можно использовать обычныеifдля условий и явно вызывать нужныеtarget.
createAction является оператором из внешнего пакета effector-action, который в ближайшем мажоре переедет в кор пакет effector. Также в дополнение нужно установить пакет patronum.
npm install effector-action patronumyarn install effector-action patronumpnpm install effector-action patronumБазовое использование
Давайте рассмотрим базовый пример на примере из начала статьи: мы хотим при срабатывании события подтверждении формы, провалидировать данные формы, если все корректно вызвать эффект с отправкой данных, а также обновить наш стор. Давай сначала рассмотрим какие юниты нам нужны:
- Нам нужно событие
submitFormдля отправки формы - Несколько сторов –
$formDataдля хранения данных формы и$formSubmittedдля статуса отправки формы - И эффект
sendFormFxчтобы отправлять данные на сервер
На странице Как мыслить в парадигме effector мы рассказываем почему стоит создавать события, а не просто вызывать эффекты напрямую из UI.
import { createEvent, createStore, sample, createEffect } from "effector";
const submitForm = createEvent();
const $formData = createStore({ username: "", age: 0 });const $formSubmitted = createStore(false);
const sendFormFx = createEffect((formData: { username: string; age: number }) => { // какая-то логика отправки данных на сервер});В UI мы будем вызывать событие submitForm, когда пользователь нажмет на кнопку отправки формы. Осталось построить связи между юнитами:
import { createEvent, createStore, sample, createEffect } from "effector";
const submitForm = createEvent();
const $formData = createStore({ username: "", age: 0 });const $formSubmitted = createStore(false);
const sendFormFx = createEffect((formData: { username: string; age: number }) => { // какая-то логика отправки данных на сервер});
sample({ clock: submitForm, source: $formData, filter: (form) => form.age >= 18 && form.username.length > 0, target: sendFormFx,});
sample({ clock: submitForm, fn: () => true, target: $formSubmitted,});import { createEvent, createStore, sample, createEffect } from "effector";
const submitForm = createEvent();
const $formData = createStore({ username: "", age: 0 });const $formSubmitted = createStore(false);
const sendFormFx = createEffect((formData: { username: string; age: number }) => { // какая-то логика отправки данных на сервер});
createAction(submitForm, { source: $formData, target: { sendForm: sendFormFx, formSubmitted: $formSubmitted, }, fn: (target, form) => { if (form.age >= 18 && form.username.length > 0) { target.sendForm(form); }
target.formSubmitted(true); },});Возможности использования
Как и говорилось оба оператора концептуально схожи друг с другом, поэтому вам не нужно делать выбор в пользу какого-либо из них, вы можете использовать в приложении и тот и другой, однако есть некоторые кейсы, когда createAction будет приоритетнее sample:
- Условная логика выполнения. При использовании
sampleможет возникнуть сложность в сужении типов послеfilter, чего нет при использованииcreateActionза счет использования нативной конструкция языка, которую TypeScript отлично понимает –if. - Группировка по триггеру. Использовать
createActionтакже удобнее, когда у нас имеется один общий триггер, но требуется разные вычисления для каждого изtarget
Давайте теперь рассмотрим основные возможности использования операторов:
import { createEvent, createStore, sample } from "effector";
const $query = createStore("");
const queryChanged = createEvent<string>();
sample({ clock: queryChanged, target: $query,});import { createStore, createEvent } from "effector";import { createAction } from "effector-action";
const $query = createStore("");
const queryChanged = createEvent<string>();
createAction(queryChanged, { target: $query, fn: (target, query) => { target(query); },});- Вы можете контролировать вызов
targetпо условию, подробнее об этом на странице API для sample:
import { createEvent, createStore, sample } from "effector";
const $query = createStore("");const $shouldUpdate = createStore(false);
const queryChanged = createEvent<string>();
sample({ clock: queryChanged, filter: $shouldUpdate, target: $query,});import { createStore, createEvent } from "effector";import { createAction } from "effector-action";
const $query = createStore("");const $shouldUpdate = createStore(false);
const queryChanged = createEvent<string>();
createAction(queryChanged, { source: { $shouldUpdate, }, target: $query, fn: (target, { shouldUpdate }, query) => { if (shouldUpdate) { target(query); } },});- Вы также можете производить вычисления в
fnфункции, однако держите в голове, что это должно быть чистой функцией, а также синхронной.
Ограничения createAction
У оператора createAction есть важное ограничение: при вызове одного и того же target несколько раз, только последний будет вызван:
import { createStore, createEvent } from "effector";import { createAction } from "effector-action";
const $counter = createStore(0);
const increase = createEvent<number>();
createAction(increase, { target: $counter, fn: (target, delta) => { target(delta); // отработает только последний вызов target target(delta + 5); },});Как использовать эти операторы
Использование этих операторов подразумевает построение атомарных связей вместо одного крупного блока кода. Для примера давайте рассмотрим еще один сценарий приложения – форма поиска с параметрами. Давайте в начале посмотрим как бы такое мы писали на ванильном js коде: Предположим у нас есть какой-то стейт в UI фреймворке:
const state = { query: "", category: "all", results: [], isLoading: false, error: null,};Функции для изменения стейта:
function handleQueryChanged(payload) { // здесь может быть любое изменение стейта из React/Vue/Solid и других фреймворков state.query = payload;}
function handleCategoryChanged(payload) { // здесь может быть любое изменение стейта из React/Vue/Solid и других фреймворков state.category = payload;}И основная функция для запроса данных:
async function handleSearchClick() { state.error = null; state.results = [];
state.isLoading = true;
try { const currentQuery = state.query; const currentCategory = state.category; // какой-то вызов api const data = await apiCall(currentQuery, currentCategory); state.results = data; } catch (e) { state.error = e.message; } finally { state.isLoading = false; }}Осталось только в UI вызывать эти функции в нужный момент. С помощью операторов sample или createAction работа обстоит чуть иначе, мы будем создавать атомарные независимые связи между юнитами. Для начала перепишем предыдущий код на юниты:
const state = { query: "", category: "all", results: [], isLoading: false, error: null,};
const $query = createStore("");const $category = createStore("all");const $results = createStore([]);const $error = createStore(null);const $isLoading = createStore(false);Нам нужны события для изменения сторов и также добавить логику изменения этих сторов:
function handleQueryChanged(payload) { state.query = payload;}
function handleCategoryChanged(payload) { state.category = payload;}
const queryChanged = createEvent<string>();const categoryChanged = createEvent<string>();
sample({ clock: queryChanged, target: $query,});
sample({ clock: categoryChanged, target: $category,});И теперь нам нужно также реализовать основную логику поиска:
async function handleSearchClick() { state.error = null; state.results = [];
state.isLoading = true;
try { const currentQuery = state.query; const currentCategory = state.category; // какой-то вызов api const data = await apiCall(currentQuery, currentCategory); state.results = data; } catch (e) { state.error = e.message; } finally { state.isLoading = false; }}
const searchClicked = createEvent();
const searchFx = createEffect(async ({ query, category }) => { const data = await apiCall(currentQuery, currentCategory); return data;});
sample({ clock: searchClicked, source: { query: $query, category: $category, }, target: searchFx,});
sample({ clock: searchFx.$pending, target: $isLoading,});
sample({ clock: searchFx.failData, fn: (error) => error.message, target: $error,});
sample({ clock: searchFx.doneData, target: $results,});В итоговом виде мы будем иметь такую модель данных:
import { createStore, createEvent, createEffect, sample } from "effector";
const $query = createStore("");const $category = createStore("all");const $results = createStore([]);const $error = createStore(null);const $isLoading = createStore(false);
const queryChanged = createEvent<string>();const categoryChanged = createEvent<string>();const searchClicked = createEvent();
const searchFx = createEffect(async ({ query, category }) => { const data = await apiCall(query, category); return data;});
sample({ clock: queryChanged, target: $query,});
sample({ clock: categoryChanged, target: $category,});
sample({ clock: searchClicked, source: { query: $query, category: $category, }, target: searchFx,});
sample({ clock: searchFx.$pending, target: $isLoading,});
sample({ clock: searchFx.failData, fn: (error) => error.message, target: $error,});
sample({ clock: searchFx.doneData, target: $results,});Связанные API и статьи
- API
- Статьи
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.