How to make useEffect support async…await?

This article is the sixth in a series of in-depth ahoos source code articles, which have been organized into document- address . I think it’s not bad, give me a follow to support it, thanks.

This article has been included in the personal blog , welcome to pay attention~

background

When you use useEffect, if you use async…await… in the callback function, the following error will be reported.

Looking at the error report, we know that the effect function should return a destroy function (effect: refers to the cleanup function returned by return). If the first parameter of useEffect is passed to async, the return value becomes a Promise, which will cause react to call the destroy function. time error .

Why does React do this?

useEffect is a very important Hook in Hooks, which allows you to perform side-effect operations in function components.
It can complete the responsibilities of the life cycle in the previous Class Component. The execution timing of the function it returns is as follows:

  • The first rendering will not be cleaned up, and the next rendering will clear the previous side effects.
  • The uninstall phase also performs cleanup operations.

No matter which one it is, we don’t want this return value to be asynchronous, so we can’t predict the execution of the code, and bugs that are difficult to locate are prone to occur. So React directly restricts that it cannot support async…await… in the useEffect callback function.

How does useEffect support async…await…

Even the callback function of useEffect cannot use async…await, so I use it directly.

Approach 1: Create an asynchronous function (async…await method), and then execute the function.

 useEffect(() => {
  const asyncFun = async () => {
    setPass(await mockCheck());
  };
  asyncFun();
}, []);

Approach 2: You can also use IIFE, as follows:

 useEffect(() => {
  (async () => {
    setPass(await mockCheck());
  })();
}, []);

custom hooks

Now that we know how to solve it, we can completely encapsulate it into a hook to make the use more elegant. Let’s take a look at the useAsyncEffect of ahooks, which supports all asynchronous writing methods, including generator functions.

The idea is the same as above, the input parameter is the same as useEffect, a callback function (but this callback function supports asynchronous), and another dependency deps. Internally, it is still useEffect, which puts asynchronous logic into its callback function.

 function useAsyncEffect(
  effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  // 依赖项
  deps?: DependencyList,
) {
  // 判断是 AsyncGenerator
  function isAsyncGenerator(
    val: AsyncGenerator<void, void, void> | Promise<void>,
  ): val is AsyncGenerator<void, void, void> {
    // Symbol.asyncIterator: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator
    // Symbol.asyncIterator 符号指定了一个对象的默认异步迭代器。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环。
    return isFunction(val[Symbol.asyncIterator]);
  }
  useEffect(() => {
    const e = effect();
    // 这个标识可以通过 yield 语句可以增加一些检查点
    // 如果发现当前 effect 已经被清理,会停止继续往下执行。
    let cancelled = false;
    // 执行函数
    async function execute() {
      // 如果是 Generator 异步函数,则通过 next() 的方式全部执行
      if (isAsyncGenerator(e)) {
        while (true) {
          const result = await e.next();
          // Generate function 全部执行完成
          // 或者当前的 effect 已经被清理
          if (result.done || cancelled) {
            break;
          }
        }
      } else {
        await e;
      }
    }
    execute();
    return () => {
      // 当前 effect 已经被清理
      cancelled = true;
    };
  }, deps);
}

async…await As we have mentioned before, let’s focus on the function of the implementation of the variable canceled in the implementation.
Its role is to interrupt execution .

Some checkpoints can be added through the yield statement. If it is found that the current effect has been cleaned up, it will stop and continue to execute.

Just imagine, there is a scenario where the user operates frequently, maybe the next round of operation b has already started before this round of operation a is completed. At this time, the logic of operation a has lost its effect, so we can stop the subsequent execution and directly enter the logic execution of the next round of operation b. This canceled is an identifier used to cancel the current execution.

Is it still possible to support the useEffect cleanup mechanism?

You can see the useAsyncEffect above, the internal useEffect return function only returns the following:

 return () => {
  // 当前 effect 已经被清理
  cancelled = true;
};

This means that you pass useAsyncEffect without the useEffect return function to perform the function of clearing the side effect .

You may think that we can put the result of the effect (the callback function of useAsyncEffect ) into useAsyncEffect?

The implementation ends up looking like this:

 function useAsyncEffect(effect: () => Promise<void | (() => void)>, dependencies?: any[]) {
  return useEffect(() => {
    const cleanupPromise = effect()
    return () => { cleanupPromise.then(cleanup => cleanup && cleanup()) }
  }, dependencies)
}

This approach has been discussed in this issue , and there is a great god’s statement above that I agree with:

He believes that this delayed clearing mechanism is wrong and should be a cancellation mechanism . Otherwise, the callback function still has a chance to affect external state after the hook has been cancelled. I will also post his implementation and examples, which are actually the same as useAsyncEffect, as follows:

accomplish:

 function useAsyncEffect(effect: (isCanceled: () => boolean) => Promise<void>, dependencies?: any[]) {
  return useEffect(() => {
    let canceled = false;
    effect(() => canceled);
    return () => { canceled = true; }
  }, dependencies)
}

Demo:

 useAsyncEffect(async (isCanceled) => {
  const result = await doSomeAsyncStuff(stuffId);
  if (!isCanceled()) {
    // TODO: Still OK to do some effect, useEffect hasn't been canceled yet.
  }
}, [stuffId]);

In the final analysis, our cleanup mechanism should not rely on async functions, otherwise it is easy to find bugs that are difficult to locate .

Summary and thinking

Since useEffect is responsible for performing side-effect operations in functional components, the execution of its return value should be predictable, not an asynchronous function, so the writing method of callback function async…await is not supported.

We can encapsulate the logic of async…await inside the useEffect callback function. This is the implementation idea of ahooks useAsyncEffect, and its scope is wider. It supports all asynchronous functions, including generator function .