---
sidebar_position: 5
title: Management Effect
---

# Management Effect

The actions in the model must be pure functions and cannot have any side effects during execution. However, in real-world scenarios, we often encounter many side effects, such as: requesting data from an HTTP API to obtain state data, or modifying `localStorage` or sending events while updating the state. In Reduck, side effects are managed through the model's `effects` function.

## Modifying State via Side Effects

The most common scenario in which side effects modify the state is requesting data from an HTTP API to update state data.

Let's take a simple `todoModel` as an example. It has a side effect function `load` that requests the TODO list from a remote server. After the request succeeds, it updates the `state.items` field.

```ts
const todoModel = model('todo').define({
  state: {
    items: [],
    loading: false,
    error: null,
  },
  actions: {
    load: {
      pending(state) {
        state.loading = true;
      },
      fulfilled(state, items) {
        state.items = items;
        state.loading = false;
      },
      rejected(state, error) {
        state.error = error;
        state.loading = false;
      },
    },
  },
  effects: {
    // Promise effects
    async load() {
      return new Promise(resolve => {
        setTimeout(() => resolve(['Learn Modern.js']), 2000);
      });
    },
  },
});
```

The side effect function is uniformly defined under the `effects` field. Here we have written a `load` function that returns a Promise. After the Promise is successfully executed, it returns the TODO list `["Lerna Modern.js"]`.

Side effect functions need to work with actions to modify state. Therefore, we define a `load` object in `actions` (the name needs to be consistent with the name of the side effect function under `effects`), which includes three actions (`pending`, `fulfilled`, `rejected`) that handle the three states (`pending`, `fulfilled`, `rejected`) of the `Promise` returned by the side effect function `load`:

- `pending`: Receives the current state `state` as a parameter and sets the `loading` flag to `true` in the new state.
- `fulfilled`: Receives the current state `state` and the Promise fulfilled value `items` as parameters, and sets `items` to the parameter `items` and `loading` to `false` in the new state.
- `rejected`: Receives the current state `state` and the Promise rejected error `error` as parameters, and sets `error` to the parameter `error` and `loading` to `false` in the new state.

How do we call the effects function in the component? The effects function will be merged into the actions object, so you can call the effects function through the actions object, as shown below:

```ts
function Todo() {
  const [state, actions] = useModel(todoModel);

  useEffect(() => {
    // invoke effects function
    actions.load();
  }, []);

  if (state.loading) {
    return <div>loading....</div>;
  }

  return (
    <div>
      <div>
        {state.items.map((item, index) => (
          <div key={index}>{item}</div>
        ))}
      </div>
    </div>
  );
}
```

In the example above, the three actions `pending`, `fulfilled`, and `rejected` are generally required for HTTP requests used to obtain data. Reduck provides a utility function `handleEffect` to simplify the creation of actions in this scenario.

For this type of side effect scenario, `handleEffect` stipulates that the state structure of the model contains three fields: `result`, `error`, and `pending`, with initial values of:

```ts
{
  result: null,
  error: null,
  pending: false,
}
```

Calling `handleEffect` will return the following data structure:

```ts
{
  pending() { // ... },
  fulfilled() { // ... },
  rejected() { // ... }
}
```

This data structure is the same as the data structure of the `load` object under `actions`. The object returned by `handleEffect` actually corresponds to the three actions required by the Effects function.

We can use `handleEffect` to rewrite `todoModel`:

```ts
const todoModel = model('todo').define({
  state: {
    items: [],
    loading: false,
    error: null,
  },
  actions: {
    load: handleEffect({ result: 'items' }),
  },
  effects: {
    // Promise effects
    async load() {
      return new Promise(resolve => {
        setTimeout(() => resolve(['Learn Modern.js']), 2000);
      });
    },
  },
});
```

The `handleEffect` function sets `result` to `items` in the received parameter object. Because `todoModel` uses `items` as the key to save the TODO list data instead of using the default `result` as the key of `handleEffect`, configuration is required here.

It is obvious that the `todoModel` implemented through `handleEffect` is much more concise than the previous implementation.

If you do not want all three states (pending, fulfilled, rejected) to be automatically handled by `handleEffect`, for example, if the fulfilled state requires more complex data processing and needs to be manually handled, but you still want to automate the handling of the pending and rejected states, you can use the following method:

```ts
  actions: {
    load: {
      ...handleEffect(),
      fulfilled(state, payload) {
        // manual handle
      },
    },
  },
```

`handleEffect` [API](/apis/app/runtime/model/handle-effect).

In the Effects function, you can also manually call Actions. For example:

```ts
const todoModel = model('todo').define((context, utils) => ({
  state: {
    items: [],
    loading: false,
    error: null,
  },
  actions: {
    pending(state) {
      state.loading = true;
    },
    fulfilled(state, items) {
      state.items = items;
      state.loading = false;
    },
  },
  effects: {
    async load() {
      // use utils.use get cuttent Model actions
      const [, actions] = utils.use(todoModel);
      // invoke action
      actions.pending();

      return new Promise(resolve => {
        setTimeout(() => {
          const items = ['Learn Modern.js'];
          // invoke action
          actions.fulfilled(items);
          resolve(items);
        }, 2000);
      });
    },
  },
}));
```

You can use the `use` function to load other Models (including the Model itself) to achieve [Model communication](/guides/topic-detail/model/model-communicate).

## Side effects do not affect state

In some cases, we only need to read State and execute relevant side effect logic, and the side effect will not modify State.

For example, store some State in `localStorage`:

```ts
const fooModel = model('foo').define((context, utils) => ({
  state: {
    value: 'foo',
  },
  effects: {
    setLocalStorage(key) {
      const [state] = utils.use(fooModel);
      localStorage.set(key, state.value);
      return 'success';
    },
  },
}));
```

or send message to server:

```ts
const fooModel = model('foo').define({
  state: {
    value: 'foo',
  },
  effects: {
    async sendData(data) {
      const res = await fetch('url', {
        method: 'POST',
        body: data,
      });
      return res.json();
    },
  },
});
```

Sometimes, we want to execute subsequent logic directly based on the execution result of the side effect function. In this case, we need to use the return value of the Effects function.

For example, when the send button is clicked and the data is successfully sent, close the current popup immediately; if it fails, display an error message. We can achieve this through the following code:

```ts
// The code is for illustration only and cannot be executed.
// Response function of the "send" button within the component.
const handleClick = async () => {
  // sendData returns a string that represents the state.
  const result = await actions.sendData('some data');
  if (result === 'success') {
    // Close the popup window.
    closeModal();
  } else {
    // show error
    showError(result);
  }
};
```

:::info
[Example](https://github.com/web-infra-dev/modern-js-examples/tree/main/series/tutorials/runtime-api/model/effects)
:::
