Как реагировать на события модели в UI
Иногда у вас может возникнуть необходимость что-то сделать на уровне UI фреймворка при вызове события в модели данных. Например, вы хотите показать оповещение когда запрос на получение данных завершился ошибкой.
Описание проблемы
В этой статье мы будем использовать React в качестве примера UI фреймворка. Однако те же принципы могут быть применены к любому другому UI фреймворку.
Давайте представим, что у нас есть приложение, которое использует Ant Design и его систему оповещений. Показать оповещение на уровне UI достаточно просто:
import { notification } from "antd";
function App() { const [api, contextHolder] = notification.useNotification();
const showNotification = () => { api.info({ message: "Hello, React", description: "Notification from UI-layer", }); };
return ( <> {contextHolder} <button onClick={showNotification}>Show notification</button> </> );}
Но, мы хотим показывать оповещение когда запрос на получение данных завершился ошибкой. При этом, весь поток данных приложения не должен быть доступен на уровне UI. Нам нужно найти способ реагировать на вызов событий в модели данных не раскрывая всю модель.
Давайте представим, что у нас есть событие, которое отвечает за ошибку при загрузке данных:
import { createEvent } from "effector";
const dataLoadingFailed = createEvent<{ reason: string }>();
Наше приложение вызывает это событие каждый раз, когда запрос на получение данных завершается ошибкой.
Решение проблемы
Нам как-то нужно связать dataLoadingFailed
и notification.useNotification
.
Давай посмотрим на идеальное решение этой проблемы, а также на пару не очень хороших решений.
Сохранить notification
инстанс в стор
Лучший способ - сохранить API-инстанс notification
в стор и использовать его через эффект. Давайте создадим пару новых юнитов для этого.
import { createEvent, createStore, sample } from "effector";
// Мы будем использовать инстанс из этого стора в приложенииconst $notificationApi = createStore(null);
// Это событие должно вызываться каждый раз, когда создается новый инстанс notification APIexport const notificationApiChanged = createEvent();
// Сохраняем новый инстанс в сторsample({ clock: notificationApiChanged, target: $notificationApi,});
Теперь нам нужно вызывать notificationApiChanged
, чтобы сохранить инстанс notification
API в стор $notificationApi
.
import { notification } from "antd";import { useEffect } from "react";import { useUnit } from "effector-react";
import { notificationApiChanged } from "./notifications";
function App() { // Используем useUnit чтобы получить событие из модели const onNewApiInstance = useUnit(notificationApiChanged); const [api, contextHolder] = notification.useNotification();
// вызываем onNewApiInstance на каждое изменение api useEffect(() => { onNewApiInstance(api); }, [api]);
return ( <> {contextHolder} {/* ...остальное приложение */} </> );}
После этого мы имеем валидный стор $notificationApi
с инстансом notification
API. Мы можем использовать его в любом месте приложения. Давайте создадим пару эффектов, чтобы удобно с ним работать.
import { attach } from "effector";
// ...
export const showWarningFx = attach({ source: $notificationApi, effect(api, { message, description }) { if (!api) { throw new Error("Notification API is not ready"); }
api.warning({ message, description }); },});
Теперь эффект showWarningFx
можно использовать в любом месте приложения без дополнительной возни.
import { createEvent, sample } from "effector";
import { showWarningFx } from "./notifications";
const dataLoadingFailed = createEvent<{ reason: string }>();
// Вызываем showWarningFx когда происходит dataLoadingFailedsample({ clock: dataLoadingFailed, fn: ({ reason }) => ({ message: reason }), target: showWarningFx,});
Теперь у нас есть валидное решение для обработки событий на уровне UI без раскрытия всего потока данных. Такой подход вы можете использовать для любых UI API, даже положить инстанс роутера в стор и управлять им из модели данных.
Однако , если вы хотите узнать, почему другие (возможно более очевидные) решения не так хороши, вы можете прочитать о них ниже.
Плохое решение №1
Плохое решение номер один - использовать глобальный инстанс notification
.
Ant Design позволяет использовать глобальный инстанс notification.
import { createEvent, createEffect, sample } from "effector";import { notification } from "antd";
const dataLoadingFailed = createEvent<{ reason: string }>();
// Создаем эффект для показа оповещенияconst showWarningFx = createEffect((params: { message: string }) => { notification.warning(params);});
// Вызываем showWarningFx когда происходит dataLoadingFailedsample({ clock: dataLoadingFailed, fn: ({ reason }) => ({ message: reason }), target: showWarningFx,});
В этом решение невозможно использовать какие-либо настройки Ant из React Context, потому что у него нет доступа к React вообще. Это значит, что оповещения не будут стилизованы должным образом и могут выглядеть иначе, чем остальная часть приложения.
Так что, это не решение.
Плохое решение №2
Второй плохое решение – использовать метод .watch
события в компоненте.
Можно вызвать метод .watch
события
import { useEffect } from "react";import { notification } from "antd";
import { dataLoadingFailed } from "./model";
function App() { const [api, contextHolder] = notification.useNotification();
useEffect( () => dataLoadingFailed.watch(({ reason }) => { api.warning({ message: reason, }); }), [api], );
return ( <> {contextHolder} {/* ...остальное приложение */} </> );}
Но в этом решении мы не соблюдаем правила для scope, а это значит, что у нас могут быть утечки памяти, проблемы с тестовой средой и инструментами типа Storybook.
Так что, это не решение.
Связанные API и статьи
-
API
-
Статьи
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.