Handle events in UI-frameworks
Sometimes you need to do something on UI-framework layer when an event is fired on effector layer. For example, you may want to show a notification when a request for data is failed. In this article, we will look into a way to do it.
The problem
In this article, we will use React as an example of a UI-framework. However, the same principles can be applied to any other UI-framework.
Let us imagine that we have an application uses Ant Design and its notification system. It is pretty straightforward to show a notification on UI-layer
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> </> );}
But what if we want to show a notification when a request for data is failed? The whole data-flow of the application should not be exposed to the UI-layer. So, we need to find a way to handle events on UI-layer without exposing the whole data-flow.
Let us say that we have an event responsible for data loading failure:
import { createEvent } from "effector";
const dataLoadingFailed = createEvent<{ reason: string }>();
Our application calls it every time when a request for data is failed, and we need to listen to it on UI-layer.
The solution
We need to bound dataLoadingFailed
and notification.useNotification
somehow.
Let us take a look on a ideal solution and a couple of not-so-good solutions.
Save notification
instance to a store
The best way is saving notification
API-instance to a store and using it thru effect. Let us create a couple new units to do it.
import { createEvent, createStore, sample } from "effector";
// We will use instance from this Store in the applicationconst $notificationApi = createStore(null);
// It has to be called every time when a new instance of notification API is createdexport const notificationApiChanged = createEvent();
// Save new instance to the Storesample({ clock: notificationApiChanged, target: $notificationApi,});
Now we have to call notificationApiChanged
to save notification
API-instance to store $notificationApi
.
import { notification } from "antd";import { useEffect } from "react";import { useUnit } from "effector-react";
import { notificationApiChanged } from "./notifications";
function App() { // use useUnit to respect Fork API rules const onNewApiInstance = useUnit(notificationApiChanged); const [api, contextHolder] = notification.useNotification();
// call onNewApiInstance on every change of api useEffect(() => { onNewApiInstance(api); }, [api]);
return ( <> {contextHolder} {/* ...the rest of the application */} </> );}
After that, we have a valid store $notificationApi
with notification
API-instance. We can use it in any place of the application. Let us create a couple effects to work with it comfortably.
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 }); },});
Effect showWarningFx
can be used in any place of the application without any additional hustle.
import { createEvent, sample } from "effector";
import { showWarningFx } from "./notifications";
const dataLoadingFailed = createEvent<{ reason: string }>();
// Show warning when dataLoadingFailed is happenedsample({ clock: dataLoadingFailed, fn: ({ reason }) => ({ message: reason }), target: showWarningFx,});
Now we have a valid solution to handle events on UI-layer without exposing the whole data-flow. This approach you can use for any UI API, even put a router in the framework and manage it from the data model.
However, if you want to know why other (maybe more obvious) solutions are not so good, you can read about them below.
Bad solution β1
Bad solution number one is using global instance of notification
.
Ant Design allows using global notification instance.
import { createEvent, createEffect, sample } from "effector";import { notification } from "antd";
const dataLoadingFailed = createEvent<{ reason: string }>();
// Create an Effect to show a notificationconst showWarningFx = createEffect((params: { message: string }) => { notification.warning(params);});
// Execute it when dataLoadingFailed is happenedsample({ clock: dataLoadingFailed, fn: ({ reason }) => ({ message: reason }), target: showWarningFx,});
In this solution it is not possible to use any Antβs settings from React Context, because it does not have access to the React at all. It means that notifications will not be styled properly and could look different from the rest of the application.
So, this is not a solution.
Bad solution β2
Bad solution number two is using .watch
method of an event in a component.
It is possible to call .watch
method of an event in a component.
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} {/* ...the rest of the application */} </> );}
In this solution we do not respect rules for scope, it means that we could have memory leaks, problems with test environments and Storybook-like tools.
So, this is not a solution.
Related APIs and Articles
-
API
-
Articles