暂挂数据提取中的UseRef

时间:2020-10-07 10:53:51

标签: reactjs react-suspense use-ref

我正在尝试使用实验性的新React功能Suspense for data fetching

这是我简单的useApi钩子(如果我正确理解了Suspense的话),该钩子返回fetch调用的结果或抛出悬挂器承诺。 (稍作修改的the documented example

function useApi(path) {
  const ref = React.useRef({ time: +new Date() });
  if (!ref.current.suspender) {
    ref.current.suspender = fetch(path).then(
      data => ref.current.data = data,
      error => ref.current.error = error,
    );
  }
  if (ref.current.data) return ref.current.data;
  if (ref.current.error) return ref.current.error;
  throw ref.current.suspender;
}

我正在使用这个钩子,就像这样:

function Child({ path }) {
  const data = useApi(path);
  return "ok";
}
export default function App() {
  return (
    <Suspense fallback="Loading…">
      <Child path="/some-path" />
    </Suspense>
  );
}

永远无法解决。

我认为问题在于useRef不能正常工作。

如果我使用随机值初始化ref,它将不保留该值,而是使用另一个随机值重新初始化:

const ref = React.useRef({ time: +new Date() });
console.log(ref.current.time)
1602067347386
1602067348447
1602067349822
1602067350895
...

抛出suspender导致useRef在每次通话时重新初始化,这有些奇怪。

throw ref.current.suspender;

如果我删除了该行,useRef可以正常工作,但是显然挂起不起作用。

我可以使其工作的另一种方法是,如果我在React之外使用某种自定义缓存,例如:

const globalCache = {}
function useApi(path) {
  const cached = globalCache[path] || (globalCache[path] = {});
  if (!cached.suspender) {
    cached.suspender = ...
  }
  if (cached.data) ...;
  if (cached.error) ...;
  throw cached.suspender;
}

这也可以使它工作,但是我宁愿使用React本身提供的东西来缓存特定于组件的数据。

我是否缺少有关useRef应该如何或不应该与Suspense一起使用的信息?

复制:https://codesandbox.io/s/falling-paper-shps2

2 个答案:

答案 0 :(得分:1)

让我们回顾一下React.Suspense上的一些事实:

  1. {<1>}的children元素要在抛出承诺得到解决之前才能安装。
  2. 您必须从函数主体(而不是从类似React.Suspense的回调中)发出诺言。

现在,您从自定义钩子中抛出了useEffect,但是根据promise,该组件从不挂载,因此,当承诺解决时,您将再次抛出承诺-无限循环。

根据1.,即使您尝试将诺言保存为状态或ref等,也仍然无法正常工作-无限循环。

因此,如果您要编写一些自定义钩子,则确实需要使用任何数据结构(可以{2.}或globalCache父级进行全局管理)指示是否已经抛弃了来自特定React.Suspense的承诺(这正是React.Suspense在Facebook代码库中所做的工作)。

答案 1 :(得分:1)

我一直在为同样的问题苦苦挣扎,但我认为实际上可以实现您想要的。我查看了 react-async 和 SWR 的实现,并注意到 react-async 实际上不会在第一次渲染时抛出,而是使用 useEffect(...) 来启动异步操作,并结合 setState触发另一个渲染,然后在后续渲染中抛出承诺(直到它解决)。我相信 SWR 实际上是一样的,只有一个细微的区别; SWR 使用 useLayoutEffect(服务器端渲染回退到 useEffect),它有一个主要好处:没有数据的初始渲染永远不会发生。

这确实意味着父组件仍然必须处理大量数据。第一次渲染可用于启动承诺,但仍必须返回而不抛出以避免无限循环。只有在第二次渲染时才会抛出承诺,这实际上会暂停渲染。