# Composing Custom Effects

### Being composable

Use the core effects is meant to be easy. That's because they are composable, which means you could nest the effects inside each other. For example, if you want to run an action through a thunk and promise, you could easily write:

```javascript
effects.thunk(() => (
  effects.defer((resolve) => {
    resolve(effects.act((state) => [state, effects.none()]);
  })
))
```

Of course, if you do this frequently, it's probably a good time to wrap up the common pattern in a custom effect.

## Composing Complex Effects

Let's start off with a larger effect, one that I actually wrote for a demo project, and we'll go through the refactoring steps I had to making this two composable and powerful effects.

{% code title="noncomposableNotificationEffect.js" %}

```javascript
import { effects } from 'ferp';

export const notificationEffect = (title, body) => effects.defer(
  Notification.requestPermission()
    .then((result) => {
      if (result === 'granted') {
        const notification = new Notification(title, { body });
      }
    })
    .catch(() => {})
    .then(() => {
      return effects.none();
    });
);

// usage: notificationEffect('Ferp.JS', 'Hello world!');
```

{% endcode %}

This is not awful, but it's also not great. There is no way to react to different permission settings, and the permission request is locked to showing individual notifications. My first refactor was to separate out the permission request from the actual notification:

{% code title="notificationEffects.js" %}

```javascript
import { effects } from 'ferp'

export const notificationPermissionEffect = (
  grantEffect,
  denyEffect = effects.none,
  dismissEffect = effects.none,
  errorEffect = effects.none
) => effects.defer(
  Notification.requestPermission()
    .then((result) => {
      switch (result) {
        case 'granted':
          return grantEffect();
        case 'denied':
          return denyEffect();
        default:
          return dismissEffect();
      }
    })
    .catch(errorEffect)
);

export const notificationEffect = (title, body) => notificationPermissionEffect(
  () => effects.none(new Notification(title, { body })),
);

// usage: notificationEffect('Ferp.JS', 'Hello world');
```

{% endcode %}

That feels better and keeps the same signature as the first, but the notification effect will eat permission denied, dismissed and error, and it's limited in how I can react to the notification further. Let's explore an alternative composition:

{% code title="composableNotificationEffects.js" %}

```javascript
import { effects } from 'ferp'

export const notificationPermissionEffect = (
  grantEffect,
  denyEffect = effects.none,
  dismissEffect = effects.none,
  errorEffect = effects.none
) => effects.defer(
  Notification.requestPermission()
    .then((result) => {
      switch (result) {
        case 'granted':
          return grantEffect();
        case 'denied':
          return denyEffect();
        default:
          return dismissEffect();
      }
    })
    .catch(errorEffect)
);

export const notificationEffect = (title, options = {}, resultEffect = effects.none) => effects.thunk(() =>
  resultEffect(new Notification(title, options))
);

// Usage:
// notifiationPermissionEffect(notificationEffect('Ferp.JS', { body: 'Hello world!' }))
```

{% endcode %}

This is a little more dense, but it offers the greatest flexibility. It would be easy to compose a message effect to store the notification in the state to react to it's events in a subscription. That could look like this:

{% code title="notificationExample.js" %}

```javascript
import { app, effects, sub } from 'ferp';
import { notificationPermissionEffect, notificationEffect } from './composableNotificationEffects.js';

const INITIAL_STATE = {
  notifications: [],
};

const NotificationAdd = (notification) => (state) => [
  { ...state, notifications: state.notifications.concat(notification) },
  effects.none(),
];

const NotificationRemove = (notification) => (state) => [
  { ...state, notifications: state.notifications((n) => n !== notification) },
  effects.none(),
];

const NotificationCreate = (title) => (state) => [
  state,
  notificationEffect(title, undefined, (notification) => effects.act(NotificationAdd, notification)),
];

const delayEffect = (timeout, nextEffect) effects.thunk(() => effects.defer((resolve) => {
  setTimeout(resolve, timeout, nextEffect);
});

const notificationSubscription = (notification, onInteract) => (dispatch) => {
  const onClose = (event) => {
    dispatch(onInteract(notification));
  };
  
  const onClick = (event) => {
    dispatch(onInteract(notification));
  };
  
  notification.addEventListener('click', onClick);
  notification.addEventListener('close', onClose);
  
  return () => {
    notification.removeEventListener('click', onClick);
    notification.removeEventListener('close', onClose);
  };
};


app({
  init: [
    INITIAL_STATE,
    effects.batch([
      notifyEffect('First Notification'),
      delayEffect(1000, () => effects.batch([
        notifyEffect('Second Notification')),
        delayEffect(3000, () => notifyEffect('Third Notification')),
      ]),
    ]),
  ],
  subscribe: (state) => [
    ...state.notifications.map(notification => sub(
      notificationSubscription,
      notification,
      NotificationRemove,
    )),
  ],
});
```

{% endcode %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ferp.mrbarry.com/understanding-effects-in-ferp/custom-effects.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
