在Taro项目中使用Suspense

145 阅读2分钟

需求

尝试在Taro项目中使用Suspense,需要一个钩子处理promise对象.useSWR的suspense模式可以实现这个效果,但与需求不是很吻合,故决定自己写一个

尝试

初版

function usePromise<T>(fn: () => Promise<T>): T {
  const resRef = useRef<T>();
  const promise = useMemo(() => {
    return new Promise<void>((resolve) => {
      resRef.current = undefined;
      fn().then((res) => {
        resRef.current = res;
        resolve();
      });
    });
  }, [fn]);
  if (!resRef.current) throw promise;
  return resRef.current;
}

如果存在promise的结果,返回这个结果;否则,中断渲染.利用useMemo,确保这个钩子总是返回最新的结果.然而,进行测试时发现组件总是展示fallback.
查阅资料后发现,如果组件第一次渲染就中断,则不会为它生成fiber.这是合理的,因为不能确保渲染中断时组件内部的每个hook都在fiber上创建了存储,这样的fiber时不符合约定的.因此每次抛出的promise进入fulfilled后,组件都会重新构建,再次抛出异常.
第二版

function usePromise<T>(fn: () => Promise<T>): T | undefined {
  const resRef = useRef<T>();
  const promiseRef = useRef<Promise<any>>();
  const prevFnRef = useRef<() => Promise<T>>();
  const [, update] = useReducer(() => ({}), {});
  if (promiseRef.current) {
    throw promiseRef.current;
  }
  if (prevFnRef.current !== fn) {
    resRef.current = undefined;
    prevFnRef.current = fn;
    promiseRef.current = fn().then((res) => {
      resRef.current = res;
      promiseRef.current = undefined;
    });
    update();
  }
  return resRef.current;
}

先返回undefined 在钩子末尾引发更新.这就和useSWR很像了 不过我还是决定把这个钩子完成.测试结果是,组件依旧展示fallback.这说明在组件函数中进行的setState被视作当前渲染流程的一部分.想要达到目标效果 应当从effect着手
第三版

export function usePromise<T>(fn: () => Promise<T>): T | undefined {
  const resRef = useRef<T>();
  const errorRef = useRef<unknown>();
  const [promise, setPromise] = useState<Promise<void>>();
  useEffect(() => {
    resRef.current = undefined;
    errorRef.current = undefined;
    setPromise(
      fn().then(
        (res) => {
          resRef.current = res;
          setPromise(undefined);
        },
        (error) => {
          errorRef.current = error;
        }
      )
    );
  }, [fn]);
  if (errorRef.current) {
    throw errorRef.current;
  }
  if (promise) {
    throw promise;
  }
  return resRef.current;
}

在useEffect中设置promise并引发更新,此时fiber已经创建,抛出异常不会导致组件重建.此外还做了错误收集.

总结

第三版已经可以达到目标,但仍有不足.在后续的更新中仍旧采用useEffect更新,这可能导致多余的渲染.仅通过函数变化重新取得promise,应当提供同一个函数重新创建promise的功能.