副作用管理

Model 中的 Action 必须是一个纯函数,执行过程中不会产生任何副作用。但在真实的业务中,我们会遇到很多副作用场景,如:请求 HTTP 接口获取状态数据,或者在更新状态的同时修改 localStorage、发送事件等。在 Reduck 中,是通过 Model 的 Effects 函数管理副作用的。

副作用对 State 修改

副作用修改 State,最常见的场景就是请求 HTTP 接口,更新状态数据。

我们以一个简单的 todoModel 为例。其有一个 load 的副作用函数,请求远端的 TODO 列表,请求成功之后更新 state.items 字段。

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 副作用
    async load() {
      return new Promise(resolve => {
        setTimeout(() => resolve(['Learn Modern.js']), 2000);
      });
    },
  },
});

副作用函数统一定义在 effects 字段下。这里我们写了一个 load 函数,它返回一个 Promise,Promise 执行成功后,返回 TODO 列表 ["Lerna Modern.js"]

副作用函数需要和 actions 配合使用,才能完成状态的修改。因此,我们在 actions 中定义了一个 load(命名需要和 effects 下的副作用函数的名字保持一致)对象,包含 pendingfulfilledrejected 3 个 action,分别是对副作用函数 load 返回的 Promise 的三种状态( pending、fulfilled、rejected )的处理:

  • pending:接收当前状态 state 作为参数,新的状态中 loading 设为 true
  • fulfilled:接收当前状态 state 和 Promise fulfilled 状态的值 items 为参数,新的状态中 items 等于参数 itemsloading 设为 false
  • rejected:接收当前状态 state 和 Promise rejected 状态的错误 error 为参数,新的状态中 error 等于参数 errorloading 设为 false

组件中如何调用 effects 函数呢? effects 函数会被合并到 actions 对象上,因此可以通过 actions 对象调用 effects 函数,如下所示:

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

  useEffect(() => {
    // 调用 effects 函数
    actions.load();
  }, []);

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

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

上面的示例中, pendingfulfilledrejected 3 个 action,对于用于获取数据的 HTTP 请求场景下,一般都是需要的。Reduck 提供了一个工具函数 handleEffect,用于简化这种场景下的 action 创建。

handleEffect 约定这种副作用场景下, Model 的 State 结构包含 resulterrorpending 3 个字段,初始值为:

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

调用 handleEffect 会返回如下数据结构:

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

这个数据结构和我们在 actions 下的 load 对象的数据结构是相同的。handleEffect 返回的对象,其实就是对应了 Effects 函数需要的 3 个 action。

利用 handleEffect,改写 todoModel

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

handleEffect 接收的参数对象,将 result 设置为 item。因为 todoModel 的 state,使用 items 作为 key 保存 todo 列表数据,而不是使用 handleEffect 默认的 result 作为 key,所以这里需要进行配置。

明显可见,通过 handleEffect 实现的 todoModel 比之前的实现有了很大精简。

如果不希望 pending、fulfilled、rejected 3 种状态都被 handleEffect 自动处理,例如 fulfilled 需要手动处理较复杂的数据类型,但是 pending、rejected 依旧想进行自动化处理,可以参考如下写法:

actions: {
    load: {
      ...handleEffect(),
      fulfilled(state, payload) {
        // 手动处理
      },
    },
  },
补充信息

handleEffect API

Effects 函数中,也支持手动调用 Actions,例如:

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() {
      // 通过 utils.use 获取当前 Model 对象的 actions
      const [, actions] = utils.use(todoModel);
      // 手动调用 action
      actions.pending();

      return new Promise(resolve => {
        setTimeout(() => {
          const items = ['Learn Modern.js'];
          // 手动调用 action
          actions.fulfilled(items);
          resolve(items);
        }, 2000);
      });
    },
  },
}));
INFO

可以使用 use 函数加载其它 Model(包括 Model 自身),实现 Model 间通信

副作用不影响 state

有些场景下,我们只需要读取 State,执行相关副作用逻辑,副作用不会修改 State。

例如,存储某些 State 到 localStorage

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';
    },
  },
}));

或者是向服务端发送数据:

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

副作用函数返回值

有时候,我们希望能根据副作用函数的执行结果,直接执行后续逻辑。这时候,就需要使用 Effects 函数的返回。

例如,当点击发送按钮,发送数据成功后,立即关闭当前的弹窗;如果失败,显示错误信息。我们可以通过如下代码实现:

// 代码仅做示意,不能执行
// 组件内部 发送按钮 的响应函数
const handleClick = async () => {
  // sendData 返回代表状态的字符串
  const result = await actions.sendData('some data');
  if (result === 'success') {
    // 关闭弹窗
    closeModal();
  } else {
    // 显示错误
    showError(result);
  }
};
补充信息