Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 18 可中断 mount 导致 useRequest 有 cacheKey 时更新报错 #1866

Closed
zjfresh opened this issue Sep 6, 2022 · 6 comments
Closed

Comments

@zjfresh
Copy link

zjfresh commented Sep 6, 2022

问题:

页面组件异步加载时,在挂载过程触发了其他高优先级任务(如下图鼠标滑到上面的tab触发 hover),打断当前的 mount 过程

react18可中断mount导致ahooks更新bug

image

image

image

原因:

mount可中断导致 useRef 会重新初始化,进而导致被中断 mount 的 Fiber hook useUpdate 的 update 函数被重复构造的 fetchInstance = useCreation() 收集,最终在 cacheSubscribe 监听无法取消(因为没有 mount 不会触发 unmount )

export default function useCreation<T>(factory: () => T, deps: DependencyList) {
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
});
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}

update 函数被 重复 new Fetch 收集

const update = useUpdate();
const fetchInstance = useCreation(() => {
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initState),
);
}, []);

constructor(
public serviceRef: MutableRefObject<Service<TData, TParams>>,
public options: Options<TData, TParams>,
public subscribe: Subscribe,
public initState: Partial<FetchState<TData, TParams>> = {},
) {

setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}

然后被会话变量 cacheSubscribe.subscribe 注册(每次的 Fetch 实例都会被注册到一个内存变量中,直到 unmount 或刷新页面)

unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
fetchInstance.setState({ data: d });
});

const listeners: Record<string, Listener[]> = {};

const subscribe = (key: string, listener: Listener) => {
if (!listeners[key]) {
listeners[key] = [];
}
listeners[key].push(listener);
return function unsubscribe() {
const index = listeners[key].indexOf(listener);
listeners[key].splice(index, 1);
};
};

debugger log:

image

综上,在被中断 mount 时,此次更新的 fiber 的 强制渲染 update 函数随着 Fetch 实例 被注册到一个内存变量中 { cacheKey: [] },在下次正常 mount 完成请求后触发对应 cacheKey 的所有 Fetch 实例更新导致 update 更新在一个 hasn't mounted component

测试仓库: https://github.com/zjfresh/umi4-umi-max

@zjfresh
Copy link
Author

zjfresh commented Sep 6, 2022

考虑给 useUpdate 做容错?试了下用 mountedRef 来判断可以避免
image

image

zjfresh pushed a commit to zjfresh/hooks that referenced this issue Sep 6, 2022
@brickspert
Copy link
Collaborator

  1. 你说的 React18 可中断机制,确实会有问题,这个我们也在规划处理,有兴趣可以一起来讨论
  2. 没太明白 useUpdate 为什么要处理,useUpdate 没有使用 ref,为什么会有问题呢?

@zjfresh
Copy link
Author

zjfresh commented Sep 7, 2022

这个 issue 的问题主要不是 useUpdate 引起的

而我是觉得既然 React 存在 render/update 可中断的的情况,那么 useUpdate 作为一个对外的 hook,也应该考虑对这种情况的兼容

保证 update() 执行是在被中断的情况下,是否应该有容错呢?因为这种在 unmount 的 Component 中进行更新产生了报错是 React 的渲染机制导致的,不是用户的不规范编码导致的

这个可以讨论看下,我也不确定这种报错是否可以算作是用户的编码不规范导致的,ahook useUpdate 不作容错,让 React 正常报错

搞了个 Demo 试了下直接在 Component 内使用是没事,但放在另一个 hook 内的复杂场景是会出错的,如上面的 useRequest

CodeSandbox 上的在线演示也可以模拟出来,在控制台看到报错 Link

import { useCallback, useEffect, useRef, useState } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { useUpdate, useMount, useCreation } from 'ahooks';

const DocsPage = () => {
  const countRef = useRef(0);
  console.log('🚀 ~ file: index.tsx ~ line 7 ~ countRef pre', countRef.current);
  countRef.current = countRef.current + 1;

  console.log('🚀 ~ file: index.tsx ~ line 10 ~ countRef after', countRef.current);

  const data = useCustom(
    new Promise((requestBack) => {
      setTimeout(() => {
        requestBack('requestData');
      }, 1500);
    })
  );

  useEffect(() => {
    console.log('useEffect [] 是只执行了一次');
  }, []);

  console.time('模拟耗时的 render ');
  let n = 0;
  while (true) {
    window.getComputedStyle(document.body);
    n++;
    if (n > 999999) break;
  }
  console.timeEnd('模拟耗时的 render ');

  return (
    <PageContainer>
      <p>This is umi docs.</p>
    </PageContainer>
  );
};

function useCustom(service: PromiseLike<string>) {
  const update = useUpdate();
  const dataRef = useRef<string | null>(null);

  useCreation(() => {
    service.then((data) => {
      dataRef.current = data;
      update();
    });
  }, []);
}

export default DocsPage;

image

@zjfresh zjfresh closed this as completed Sep 7, 2022
@zjfresh zjfresh reopened this Sep 7, 2022
@brickspert
Copy link
Collaborator

这个可以讨论看下,我也不确定这种报错是否可以算作是用户的编码不规范导致的,ahook useUpdate 不作容错,让 React 正常报错

这个报错是正常的,React18 已经解释过,这个报错是可以忽略的。 看下这个:brickspert/blog#47

@zjfresh
Copy link
Author

zjfresh commented Sep 9, 2022

这个可以讨论看下,我也不确定这种报错是否可以算作是用户的编码不规范导致的,ahook useUpdate 不作容错,让 React 正常报错

这个报错是正常的,React18 已经解释过,这个报错是可以忽略的。 看下这个:brickspert/blog#47

brickspert/blog#47 这个链接与上面的问题在给出的处理结果上可能是一致的(允许在 unmounted 的组件 setState)

但具体可能还是不太一样,官方说的是 mount 后 unmount 引起 setState,上面的是 mount 中断后重新 mount 引起的第一个已经没意义的 mount fiber 中的 setState,像提到的以后允许在 umounted 的组件可以正常 setState,但这种未正常 mount 的组件我觉得应该不在这个范围吧

而上面提到的报错可能还是会令人感到困惑,毕竟那个只是告警都作了移除以避免误导,而这个错误还是在 [email protected]

image

不清楚官方对这个有没有计划,或许这个错误该放到 React 18 中去讨论 👀

@zjfresh zjfresh closed this as completed Sep 19, 2022
@keyus
Copy link

keyus commented Dec 27, 2023

这个ahook是不是考虑,升级一下,现在的性能确实稍差,比起swr还是有些不如, 主要还是缓存那块,问题有些大啊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants