需求
尝试在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的功能.