Skip to content

Handle Effector's 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 ​

TIP

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

tsx
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:

ts
// model.ts
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.

ts
// notifications.ts
import { createEvent, createStore, sample } from 'effector';

// We will use instance from this Store in the application
const $notificationApi = createStore(null);

// It has to be called every time when a new instance of notification API is created
export const notificationApiChanged = createEvent();

// Save new instance to the Store
sample({ clock: notificationApiChanged, target: $notificationApi });

Now we have to call notificationApiChanged to save notification API-instance to Store $notificationApi.

tsx
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.

ts
// notifications.ts
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 });
  },
});

TIP

attach is a function that allows to bind specific Store to an Effect. It means that we can use notificationApi in showWarningFx without passing it as a parameter. Read more in Effector's documentation.

Effect showWarningFx can be used in any place of the application without any additional hustle.

ts
// model.ts
import { createEvent, sample } from 'effector';

import { showWarningFx } from './notifications';

const dataLoadingFailed = createEvent<{ reason: string }>();

// Show warning when dataLoadingFailed is happened
sample({
  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.

However, if you want to know why other (maybe more obvious) solutions are not so good, you can read about them below 👇

Not-so-good solutions

🔴 Global notification service ​

Ant Design allows using global notification instance.

ts
// model.ts
import { createEvent, createEffect, sample } from 'effector';
import { notification } from 'antd';

const dataLoadingFailed = createEvent<{ reason: string }>();

// Create an Effect to show a notification
const showWarningFx = createEffect((params: { message: string }) => {
  notification.warning(params);
});

// Execute it when dataLoadingFailed is happened
sample({
  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.

🔴 Just .watch an Event in a component ​

It is possible to call .watch-method of an Event in a component.

tsx
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 Fork API rules, it means that we could have memory leaks, problems with test environments and Storybook-like tools.

So, this is not a solution.

Summary ​

To bind some UI-framework specific API to Effector's data-flow we need to follow these steps:

  1. Retrieve API-instance from the framework.
  2. Save it to a Store.
  3. Create an Effect to work with it.
  4. Use this Effect in the application.

Released under the MIT License.