[JavaScript, Интерфейсы, ReactJS, TypeScript] Использование Effector в стеке React + TypeScript
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет! Меня зовут Елизавета Добрянская, я frontend-разработчик в компании ДомКлик. Моя команда занимается разработкой сервисов, предназначенных для коммуникаций с клиентом. В этой статье я поделюсь своим кратким обзором внедрения стейт-менеджера Effector в продуктовый проект на стеке React + TypeScript, а также покажу на примере, как легко это можно сделать.
Содержание:
- Немного предыстории
- Первая встреча с Effector
- Боль как начало
- Выходим на новый уровень — получаем удовольствие
- Best practices
- Итоги
- Вместо послесловия
Немного предысторииМоя команда занимается разработкой разных видов сервисов коммуникаций — отдельных виджетов, npm-пакетов, SSR, полностраничных сайтов. У всех этих продуктов есть одно важное требование: интерфейс должен быстро реагировать на действия пользователя, при этом сам сервис должен выдерживать большую нагрузку. А это значит, что на нас, как на разработчиках, лежит большая ответственность за то, как мы проектируем frontend.Перед созданием нового проекта мы с командой устроили брейншторм на предмет выбора стейт-менеджера. Нам было важно, чтобы он стал хорошим помощником в разработке, позволял быстро и удобно писать код, и плюс ко всему не «бил» по производительности нового проекта. Его главной задачей стало сохранение данных на клиенте для дальнейшей модификации и отправки на бек (ничего необычного).Выбирали между Redux, Mobx и Effector. Первые два мы пробовали, и впечатления остались очень неоднозначные. И как ясно из статьи, выбрали последний, потому что любопытно было узнать, что же за зверь такой этот Effector и чем он может помочь нам. К тому же новый проект создавался для внутренних нужд и на нем вполне можно было поэкспериментировать.ATTENTION: приведенные в статье размышления являются сугубо субъективными, поэтому ваше мнение может отличаться от моего. Они носят обозревательный характер и позволяют познакомиться с Effector на моем примере.Все примеры с кодом доступны в тестовом проекте на GitHub, который при необходимости можно запустить и лично познакомиться с Effector.Первая встреча с EffectorЧто есть Effector? Модный, молодежный реактивный стейт-менеджер :) А потому понять его базовые принципы оказалось довольно просто. В его основе лежат три простых базовых сущности:
- Хранилище (Store) — это место, где мы храним наши данные.
- Событие (Event) — это действие, которое каким-то образом модифицирует хранилище.
- Эффект (Effect) — это асинхронное действие, связанное с хранилищем.
У каждой из сущностей есть большое количество различных методов, позволяющих изменять входные/выходные параметры, связывать сущности между собой и выполнять другие крутые штуки.Основная идея, лежащая в основе Effector — подписка на события. У нас есть хранилище, мы подписываемся на его обновления, вешая определенный обработчик. Например:
// Создаем хранилище, в котором будет лежать массив пользователей
// IUser — интерфейс, описывающий пользователя (имя, фамилия и т.п.)
export const $users = createStore<IUser[]>([]);
// Создаем событие, принимающее параметр IUser
export const update = createEvent<IUser>();
// Обычный хендлер на обновление. Добавляем или изменяем пользователя
const updateStore = (state: IUser[], data: IUser) => {
const userIndex = state.findIndex((user) => user.id === data.id);
// Изменяем стейт
if (userIndex > -1) {
state.splice(userIndex, 1, data);
} else {
state.push(data);
}
// Возвращаем измененный стейт
return [...state];
};
// Подписываемся на событие в хранилище
$users
.on(update, updateStore);
Effector позволяет работать с разными типами приложений, таких как React, React Native, Vue, Node.js. Кроме того, он поддерживает TypeScript.Для работы с React есть удобный пакет effector-react, предоставляющий несколько интерфейсов взаимодействия React-компонентов с Effector. Самый простой способ — использовать хук useStore для максимально лаконичной работы с хранилищами Effector. Вот пример работы с описанным выше хранилищем $users, где по нажатию на кнопку мы добавляем в хранилище пользователя-заглушку:
import { useStore } from 'effector-react';
import { $users, update } from 'models/users';
export const UserList = () => {
const users = useStore($users);
const mockUser = {
id: 1111,
name: 'Peter',
surname: 'Jonson',
age: 25,
gender: 'male',
};
const usersItems = users.map((user) => (
<div key={user.id}>
<div>Name: {user.name}</div>
<div>Surname: {user.surname}</div>
<div>Age: {user.age}</div>
<div>Gender: {user.gender}</div>
<br/>
</div>
));
return (
<div>
{usersItems}
<button onClick={() => update(mockUser)}>
Add mock user to Effector store
</button>
</div>
);
};
Ради интереса можно попробовать сделать то же самое, но с хуком useList. Он предоставляет упрощенный вариант взаимодействия с хранилищем-массивом. Реализация аналогичной задачи:
import { useList } from 'effector-react';
import { $users, update } from 'models/users';
export const UserList2 = () => {
// Можно преобразовать в массив нод сразу при подключении.
// Не нужно использовать пропс key, как было с map()
const users = useList($users, (user) => (
<div>
<div>Name: {user.name}</div>
<div>Surname: {user.surname}</div>
<div>Age: {user.age}</div>
<div>Gender: {user.gender}</div>
<br/>
</div>
));
const mockUser = {
id: 2222,
name: 'Diana',
surname: 'Gregory',
age: 22,
gender: 'female',
};
return (
<div>
{users}
<button onClick={() => update(mockUser)}>
Add mock user to Effector store
</button>
</div>
);
};
Об этом и многом другом можно почитать в официальной документации Effector. Поэтому долго не будем на этом останавливаться и перейдем к «самому сладенькому». Далее я расскажу про боли и страдания в процессе работы с этим, казалось бы, очень простым и удобным стейт-менеджером. Без купюр.Боль как началоНе стоит думать, что статья про несовершенный, наполненный кучей проблем и багов продукт увидела бы свет. Проблемы, описанные здесь, я встретила будучи полным новичком в Effector. По итогу они были повержены, а автор этой статьи — счастлив :) Если вы встретите похожие проблемы, то можете воспользоваться приведенными решениями или модернизировать их, чтобы создать своё.1) TypeScriptДа, самым сложным для меня оказалась поддержка такого же модного и молодежного, как и Effector, языка программирования TypeScript. В официальной документации Effector-а все примеры приведены на чистом JavaScript. Есть, конечно, маленькая робкая вкладка "TypeScript", которая, в основном, даёт только понимание того, куда нужно добавить типы в описании основных сущностей, но на этом всё. Поэтому сначала я использовала any, а под конец пришлось очень много страдать с расстановкой правильных типов (особенно касательно эффектов).
Так, например, родились следующие интерфейсы функций (слабонервным не смотреть):
// Создаем эффекты для получения и изменения данных о пользователях
// IUserPayload - интерфейс пользователя, приходящий с сервера
export const getUsersFx = createEffect<void, IUserPayload[], Error>();
export const updateUserFx = createEffect<
IUserPayload,
IUserPayload,
Error
>();
// Изменяем формат данных из хранилища в формат, необходимый для отправки запроса
const serializeDataBeforeFetch = attach<
IUser,
Store<IUser[]>,
typeof updateUserFx
>({
effect: updateUserFx,
source: $users,
mapParams: (params: IUser, data: IUser[]) => {
const user = data.find((item) => item.id === params.id)!;
const userCopy = { ...user };
delete userCopy?.onlineStatus;
return userCopy;
},
});
Небольшие пояснения по коду.Эффекты имеют следующий формат типизации:
- Тип передаваемого в эффект значения.
- Тип возвращаемого из эффекта значения.
- Тип ошибки для случая, если что-то пошло не так.
Про функцию serializeDataBeforeFetch расскажу ниже, а пока стоит обратить внимание на типы метода attach, предоставляемого Effector:
- Тип передаваемого значения.
- Тип данных хранилища.
- Тип эффекта, используемого внутри attach.
2) Асинхронные событияПоначалу было очень сложно это понять и принять. Представьте ситуацию, что вы написали код, и при тестировании он выдает неожиданные результаты и ошибку. Вы пытаетесь отладить ошибкоопасное место с помощью точек останова, но видите, что в дебаг-режиме всё работает, как нужно. А вот в обычном режиме (и на самом деле) всё не так, ничего не работает. То есть в режиме отладки вы как бы «притормаживаете» свой код, и поэтому он отрабатывает корректно, а на самом деле есть проблемы. Собственно, это просто нужно принять к сведению торопливому разработчику — действия в Effector происходят асинхронно (подобно setState в React).3) Получение доступа к текущему состояниюЭтот пункт про то, что нужно внимательно смотреть документацию :)Некоторые методы в Effector могут первым параметром принимать текущее состояние хранилища, а некоторые — нет. Поэтому нужно внимательно выбирать методы обработки.4) Четкий интерфейс работы с сущностямиПочему это может быть плохо? Потому что сложно отслеживать результат изменения хранилища в рамках связанного компонента. Интерфейс взаимодействия упрощенно выглядит так:
- Хранилище — readonly. В компоненте мы на него подписываемся, и все изменения считываем реактивно.
- Событие — по сути, setter. Мы говорим «измени моё хранилище, добавь в него эти данные и удали те». Событие ничего не возвращает. Поэтому его нельзя использовать как getter и получить отфильтрованные данные из хранилища напрямую (об этом будет далее).
- Эффект — аналогичен событию, но имеет свойства .done, .fail, .pending и .finally, с которыми можно взаимодействовать (об этом тоже будет далее).
5) Отсутствие геттеровЕсли вы раньше работали с Mobx или Redux, то привыкли, что у модели можно задать геттеры и обращаться к ним для получения, например, отфильтрованных или хитро измененных данных. Как было сказано выше, в Effector такого нет. Но... Зачем нам геттер, если мы можем создать новое хранилище?Для нас привычно, что хранилище относится к модели 1 к 1. Здесь эта логика рушится в пух и прах. Мы можем создавать несколько хранилищ, связанных друг с другом, как нам нужно.Пример нового хранилища, зависимого от основного:
// Учебный пример.
// Предположим, на клиенте нужно дополнительное поле со статусом пользователя.
// Оно не приходит с сервера, и мы добавляем его искусственно.
// Добавляем поле Статус каждому пользователю
const serializeUsers = (state: IUser[]) =>
state.map((user) => ({ ...user, onlineStatus: true }));
/**
* Новое хранилище, зависимое от хранилища $users.
* Данные из $users прогоняются через функцию serializeUsers
* и сохраняются в новое хранилище, которое можно использовать в компоненте
*/
export const $usersWithStatus = $users.map(serializeUsers);
6) Отслеживание статуса эффектовУ эффектов есть промисоподобные свойства .done, .fail, .pending и .finally. Поэтому кажется, что очень удобно отслеживать статус. Но обычно он важен для отображения данных в компоненте: когда мы послали запрос на данные и ожидаем ответа, нужно показывать лоадер; когда данные загружены с ошибкой — нужно показать ошибку. Поэтому необходимо каким-то образом прокидывать эти статусы в компонент. Как было сказано выше, геттеров нет. Но есть хранилища! Можно создать хранилище, сочетающее в себе все статусы:
/* МОДЕЛЬ В EFFECTOR */
// Создаем эффект, который делает GET-запрос на бек
export const getUsersFx = createEffect<void, IUserPayload[], Error>();
// Создаем хранилище, в котором будет лежать ошибка, если GET-запрос зафейлится
// I вариант
export const $fetchError = restore<Error>(getUsersFx.failData, null);
// Создаем другое хранилище, содержащий всю информацию по GET-запросу
export const $usersGetStatus = combine({
loading: getUsersFx.pending,
error: $fetchError,
data: $users,
});
/* КОМПОНЕНТ, ИСПОЛЬЗУЮЩИЙ ХРАНИЛИЩЕ */
export const UserList3 = () => {
// Подключаем хранилище в компонент
const { loading, error, data } = useStore($usersGetStatus);
// Делаем запрос на бек на didMount
useEffect(() => {
getUsersFx();
}, []);
if (loading) {
return (
<div>Загрузка...</div>
);
}
if (error) {
return (
<div>
<span><b>Произошла ошибка: </b></span>
<span>{error.message}</span>
</div>
);
}
const usersItems = data.map((user) => (
<div key={user.id}>
<div>Name: {user.name}</div>
<div>Surname: {user.surname}</div>
<div>Age: {user.age}</div>
<div>Gender: {user.gender}</div>
<br/>
</div>
));
return (
<div>
{usersItems}
</div>
);
};
В приведённом выше варианте создания хранилища $fetchError был использован еще один метод Effector — restore. Он позволяет создать хранилище, содержимое которого будет зависеть от события наступления события. Очень удобно использовать для очистки (сброса в начальное состояние) хранилища.Создать хранилище $fetchError можно и через стандартный createStore :
// II вариант
export const $fetchError = createStore<Error | null>(null);
$fetchError
.on(getUsersFx.fail, (_, { error }) => error)
.reset(getUsersFx.done);
Выходим на новый уровень - получаем удовольствиеНесмотря на большое количество непоняток, которые может встретить начинающий эффекторец, в процессе ты понимаешь, что он очень удобный. Ниже я выделила основные пункты, которые понравились лично мне.1) Никаких лишних телодвижений для подписки на хранилищеПри грамотно созданных моделях в компоненте не нужно страдать и отслеживать все свои телодвижения по обновлению хранилища. Подключили его в компонент — он всегда актуален и перерисовывается при каждом обновлении хранилища. Никаких тебе Mobx-овых @action, @computed и прочей ручной настройки. Каеф :)2) Меньше кода (и меньше размер)Нет надобности создавать отдельные классы-модели, прописывать им интерфейсы. Создали хранилище, создали событие, подписали событие на хранилище — готово!И да, размер двух подключенных библиотек effector и effector-react составляет около 8 Кб (у Mobx сумма подключенных библиотек — около 15-20 Кб)!
3) Минимальное взаимодействие компонента и хранилищаКогда я решала похожую задачу на Mobx, у меня было очень странное взаимодействие компонента и хранилища:
- Из компонента посылаем запрос на бек (потому что нужно отслеживать статус запроса).
- Здесь же получили данные и положили их в хранилище.
Т.е. компонент используется как прокси в этом случае. И это кажется очень странным, потому что зачем? Нам нужно просто положить данные из ответа на запрос в хранилище, без взаимодействия с компонентом.Effector позволяет реализовать работу напрямую: из хранилища послал запрос, в хранилище положил ответ. И наоборот. Это, например, очень удобно делается с помощью метода forward. Мы перенаправляем выход эффекта на вход события.Для примера рассмотрим историю, когда нам нужно обновить хранилище и сразу же отправить запрос на бек. Выше был пример с добавлением искусственного поля onlineStatus в модель пользователя. Перед отправкой удалим это поле из пейлоада, т.к. бек про него ничего не знает. Описанную историю можно реализовать таким образом:
/* СОЗДАНИЕ СОБЫТИЯ */
// Создаем событие на обновление хранилища
export const update = createEvent<IUser>();
// Хендлер на обновление хранилища (был описан выше)
const updateStore = (state: IUser[], data: IUser) => {
const userIndex = state.findIndex((user) => user.id === data.id);
// Изменяем стейт
if (userIndex > -1) {
state.splice(userIndex, 1, data);
} else {
state.push(data);
}
// Возвращаем измененный стейт
return [...state];
};
// Подписываемся на обновление хранилища через хендлер
$users
.on(update, updateStore)
/**********************************************************/
/* СОЗДАНИЕ ЭФФЕКТА */
// Создаем эффект для изменения данных о пользователе (Запрос на бек)
export const updateUserFx = createEffect<IUserPayload, IUserPayload, Error>();
// Асихронная функция запроса на бек
const updateUser = async (data: IUserPayload): Promise<IUserPayload> => {
const res = await axios({
url: `/users/${data.id}`,
method: 'PATCH',
});
return res.data;
}
// Привязываем к эффекту
updateUserFx.use(updateUser);
/**********************************************************/
/* ПРЕОБРАЗОВАНИЕ ДАННЫХ */
// Изменяем формат данных из хранилища в формат, необходимый для отправки запроса
// (Удаляем искусственное поле onlineStatus)
const serializeDataBeforeFetch = attach<
IUser,
Store<IUser[]>,
typeof updateUserFx
>({
effect: updateUserFx,
source: $users,
mapParams: (params: IUser, data: IUser[]) => {
const user = data.find((item) => item.id === params.id)!;
const userCopy = { ...user };
delete userCopy?.onlineStatus;
return userCopy;
},
});
// Связываем событие и функцию-преобразователь
forward({
from: update,
to: serializeDataBeforeFetch,
});
Пояснения по коду.Стек вызова упрощенно будет выглядеть следующим образом:
- update(...) — вызываем событие на обновление хранилища из компонента.
- updateStore — хранилище обновляется согласно переданному хендлеру.
- serializeDataBeforeFetch — после обновления хранилища вызывается функция преобразования его данных в пейлоад. В ней используется метод Effector attach, позволяющий сделать forward с модификацией.
- updateUserFx — вызываем эффект на обновление.
- updateUser — делаем запрос на бек.
Вуаля!
Да, на первый взгляд это выглядит запутанно. Но если в этом разобраться, можно очень удобно использовать «перебрасывание» данных из одной функции в другую.4) Крутое и отзывчивое сообществоКогда я поняла, что в документации и гугле нужных мне примеров нет от слова совсем, я решила действовать радикально и пойти в сообщество Effector в Telegram. Я задала один вопрос «от хлебушка», на который я получила за один вечер... 5 разных вариантов решений от разных разработчиков! Причём решения были разные по уровню сложности, я могла выбрать любое из них, или скомбинировать и создать своё. Некоторые решения были очень хорошо расписаны и объяснены, некоторые содержали продуктовый код с примерами прямо на GitHub, некоторые содержали ссылки на воркшопы по Effector. В общем, я приятно удивлена, что есть такое классное сообщество, где ребята всячески поддерживают друг друга :)Да и в целом в проекте я использовала версию Effector 21.5.0. То есть ребята мажорно обновляли свой проект 20 раз. Это очень существенно!Best practices[Для тех, кто хочет знать больше] Об этом есть статья в самой документации, но я кратко продублирую.
- Названия хранилищ содержат символ $. Например, $users.
- Названия эффектов содержат суффикс Fx. Например, getUsersFx.
- Файловая структура. В корне исходников создается папка models, внутри которой лежат все модели, работающие с Effector. У каждой модели есть два файла:
- index.ts — файл, где мы объявляем все хранилища, события, эффекты. Это файл начального объявления;
- init.ts — файл, где мы описываем все хранилища, события, эффекты и связываем их между собой. Здесь вся бизнес-логика.
ИтогиВ заключение хочу заметить, что Effector выглядит наиболее приятным в использовании стейт-менеджером. Он позволяет легко разделять работу с данными по разным хранилищам и не держать всё в одном (декомпозиция лайк). В нем используется неизменяемый стейт и нет необходимости писать много дублирующегося кода, что повышает производительность вашего проекта. Effector обладает удобным API и прекрасным сообществом разработчиков, поддерживающих проект.Я определенно убеждена, что использование Effector в продуктовой разработке — одно из самых удобных решений. Особенно, если в нем разобраться глубже, чем просто на уровне новичка. Поэтому внедряйте новый стейт-менеджер в свои проекты, пишите комментарии к этой статье и давайте продолжать делать крутой веб вместе ;)
Вместо послесловияПолезные ссылки:
- Тестовый проект с представленным в статье кодом
- Документация Effector
- GitHub Effector
- Сообщество Effector в Telegram
- Новости и советы по Effector в Telegram-канале
- Почему лучше выбрать Effector, а не Mobx или Redux
- Почему нужно попробовать Effector
- Знакомство с Effector на примере TODO-листа
- 2-часовой воркшоп по Effector
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, Реверс-инжиниринг, Софт] Frida изучаем эксплуатацию алгоритмов Heap
- [JavaScript, Node.JS] Разработка сервера для многопользовательской игры с помощью nodejs и magx
- [Python] Типовые ошибки на собеседовании
- [JavaScript, Программирование, Node.JS] Декораторы в JavaScript с нуля (перевод)
- [JavaScript, Программирование, ReactJS] Начинающим React-разработчикам: приложение со списком дел (покупок) (перевод)
- [Системное программирование, Интерфейсы, Разработка под Linux, Программирование микроконтроллеров] Configuring FT4232H using the ftdi_eeprom
- [Системное программирование, Интерфейсы, Разработка под Linux, Программирование микроконтроллеров] Конфигурируем FT4232H c помощью утилиты ftdi_eeprom
- [JavaScript, Node.JS] Отправка ответа с Коа (перевод)
- [Интерфейсы] Разрабатывайте приложение для друзей
- [] Frontend в Sportmaster Lab
Теги для поиска: #_javascript, #_interfejsy (Интерфейсы), #_reactjs, #_typescript, #_effector, #_react, #_typescript, #_javascript, #_frontend, #_razrabotka_vebprilozhenij (разработка веб-приложений), #_blog_kompanii_domklik (
Блог компании ДомКлик
), #_javascript, #_interfejsy (
Интерфейсы
), #_reactjs, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 15:14
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет! Меня зовут Елизавета Добрянская, я frontend-разработчик в компании ДомКлик. Моя команда занимается разработкой сервисов, предназначенных для коммуникаций с клиентом. В этой статье я поделюсь своим кратким обзором внедрения стейт-менеджера Effector в продуктовый проект на стеке React + TypeScript, а также покажу на примере, как легко это можно сделать. Содержание:
// Создаем хранилище, в котором будет лежать массив пользователей
// IUser — интерфейс, описывающий пользователя (имя, фамилия и т.п.) export const $users = createStore<IUser[]>([]); // Создаем событие, принимающее параметр IUser export const update = createEvent<IUser>(); // Обычный хендлер на обновление. Добавляем или изменяем пользователя const updateStore = (state: IUser[], data: IUser) => { const userIndex = state.findIndex((user) => user.id === data.id); // Изменяем стейт if (userIndex > -1) { state.splice(userIndex, 1, data); } else { state.push(data); } // Возвращаем измененный стейт return [...state]; }; // Подписываемся на событие в хранилище $users .on(update, updateStore); import { useStore } from 'effector-react';
import { $users, update } from 'models/users'; export const UserList = () => { const users = useStore($users); const mockUser = { id: 1111, name: 'Peter', surname: 'Jonson', age: 25, gender: 'male', }; const usersItems = users.map((user) => ( <div key={user.id}> <div>Name: {user.name}</div> <div>Surname: {user.surname}</div> <div>Age: {user.age}</div> <div>Gender: {user.gender}</div> <br/> </div> )); return ( <div> {usersItems} <button onClick={() => update(mockUser)}> Add mock user to Effector store </button> </div> ); }; import { useList } from 'effector-react';
import { $users, update } from 'models/users'; export const UserList2 = () => { // Можно преобразовать в массив нод сразу при подключении. // Не нужно использовать пропс key, как было с map() const users = useList($users, (user) => ( <div> <div>Name: {user.name}</div> <div>Surname: {user.surname}</div> <div>Age: {user.age}</div> <div>Gender: {user.gender}</div> <br/> </div> )); const mockUser = { id: 2222, name: 'Diana', surname: 'Gregory', age: 22, gender: 'female', }; return ( <div> {users} <button onClick={() => update(mockUser)}> Add mock user to Effector store </button> </div> ); }; Так, например, родились следующие интерфейсы функций (слабонервным не смотреть): // Создаем эффекты для получения и изменения данных о пользователях
// IUserPayload - интерфейс пользователя, приходящий с сервера export const getUsersFx = createEffect<void, IUserPayload[], Error>(); export const updateUserFx = createEffect< IUserPayload, IUserPayload, Error >(); // Изменяем формат данных из хранилища в формат, необходимый для отправки запроса const serializeDataBeforeFetch = attach< IUser, Store<IUser[]>, typeof updateUserFx >({ effect: updateUserFx, source: $users, mapParams: (params: IUser, data: IUser[]) => { const user = data.find((item) => item.id === params.id)!; const userCopy = { ...user }; delete userCopy?.onlineStatus; return userCopy; }, });
// Учебный пример.
// Предположим, на клиенте нужно дополнительное поле со статусом пользователя. // Оно не приходит с сервера, и мы добавляем его искусственно. // Добавляем поле Статус каждому пользователю const serializeUsers = (state: IUser[]) => state.map((user) => ({ ...user, onlineStatus: true })); /** * Новое хранилище, зависимое от хранилища $users. * Данные из $users прогоняются через функцию serializeUsers * и сохраняются в новое хранилище, которое можно использовать в компоненте */ export const $usersWithStatus = $users.map(serializeUsers); /* МОДЕЛЬ В EFFECTOR */
// Создаем эффект, который делает GET-запрос на бек export const getUsersFx = createEffect<void, IUserPayload[], Error>(); // Создаем хранилище, в котором будет лежать ошибка, если GET-запрос зафейлится // I вариант export const $fetchError = restore<Error>(getUsersFx.failData, null); // Создаем другое хранилище, содержащий всю информацию по GET-запросу export const $usersGetStatus = combine({ loading: getUsersFx.pending, error: $fetchError, data: $users, }); /* КОМПОНЕНТ, ИСПОЛЬЗУЮЩИЙ ХРАНИЛИЩЕ */
export const UserList3 = () => { // Подключаем хранилище в компонент const { loading, error, data } = useStore($usersGetStatus); // Делаем запрос на бек на didMount useEffect(() => { getUsersFx(); }, []); if (loading) { return ( <div>Загрузка...</div> ); } if (error) { return ( <div> <span><b>Произошла ошибка: </b></span> <span>{error.message}</span> </div> ); } const usersItems = data.map((user) => ( <div key={user.id}> <div>Name: {user.name}</div> <div>Surname: {user.surname}</div> <div>Age: {user.age}</div> <div>Gender: {user.gender}</div> <br/> </div> )); return ( <div> {usersItems} </div> ); }; // II вариант
export const $fetchError = createStore<Error | null>(null); $fetchError .on(getUsersFx.fail, (_, { error }) => error) .reset(getUsersFx.done); 3) Минимальное взаимодействие компонента и хранилищаКогда я решала похожую задачу на Mobx, у меня было очень странное взаимодействие компонента и хранилища:
/* СОЗДАНИЕ СОБЫТИЯ */
// Создаем событие на обновление хранилища export const update = createEvent<IUser>(); // Хендлер на обновление хранилища (был описан выше) const updateStore = (state: IUser[], data: IUser) => { const userIndex = state.findIndex((user) => user.id === data.id); // Изменяем стейт if (userIndex > -1) { state.splice(userIndex, 1, data); } else { state.push(data); } // Возвращаем измененный стейт return [...state]; }; // Подписываемся на обновление хранилища через хендлер $users .on(update, updateStore) /**********************************************************/ /* СОЗДАНИЕ ЭФФЕКТА */ // Создаем эффект для изменения данных о пользователе (Запрос на бек) export const updateUserFx = createEffect<IUserPayload, IUserPayload, Error>(); // Асихронная функция запроса на бек const updateUser = async (data: IUserPayload): Promise<IUserPayload> => { const res = await axios({ url: `/users/${data.id}`, method: 'PATCH', }); return res.data; } // Привязываем к эффекту updateUserFx.use(updateUser); /**********************************************************/ /* ПРЕОБРАЗОВАНИЕ ДАННЫХ */ // Изменяем формат данных из хранилища в формат, необходимый для отправки запроса // (Удаляем искусственное поле onlineStatus) const serializeDataBeforeFetch = attach< IUser, Store<IUser[]>, typeof updateUserFx >({ effect: updateUserFx, source: $users, mapParams: (params: IUser, data: IUser[]) => { const user = data.find((item) => item.id === params.id)!; const userCopy = { ...user }; delete userCopy?.onlineStatus; return userCopy; }, }); // Связываем событие и функцию-преобразователь forward({ from: update, to: serializeDataBeforeFetch, });
Да, на первый взгляд это выглядит запутанно. Но если в этом разобраться, можно очень удобно использовать «перебрасывание» данных из одной функции в другую.4) Крутое и отзывчивое сообществоКогда я поняла, что в документации и гугле нужных мне примеров нет от слова совсем, я решила действовать радикально и пойти в сообщество Effector в Telegram. Я задала один вопрос «от хлебушка», на который я получила за один вечер... 5 разных вариантов решений от разных разработчиков! Причём решения были разные по уровню сложности, я могла выбрать любое из них, или скомбинировать и создать своё. Некоторые решения были очень хорошо расписаны и объяснены, некоторые содержали продуктовый код с примерами прямо на GitHub, некоторые содержали ссылки на воркшопы по Effector. В общем, я приятно удивлена, что есть такое классное сообщество, где ребята всячески поддерживают друг друга :)Да и в целом в проекте я использовала версию Effector 21.5.0. То есть ребята мажорно обновляли свой проект 20 раз. Это очень существенно!Best practices[Для тех, кто хочет знать больше] Об этом есть статья в самой документации, но я кратко продублирую.
Вместо послесловияПолезные ссылки:
=========== Источник: habr.com =========== Похожие новости:
Блог компании ДомКлик ), #_javascript, #_interfejsy ( Интерфейсы ), #_reactjs, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 15:14
Часовой пояс: UTC + 5