Suspense 数据抓取中的 UseRef

UseRef in Suspense data fetching

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

这是我的简单 useApi 挂钩,它(如果我正确理解 Suspense)要么 returns 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 会按预期工作,但显然 Suspense 不起作用。

我可以让它工作的另一种方法是,如果我在 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

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

  1. React.Suspensechildren 元素在 thrown promise 解决之前不会挂载。
  2. 您必须从函数体中抛出承诺(而不是像 useEffect 这样的回调)。

现在,您从自定义挂钩中抛出 promise,但根据 1. 组件永远不会安装,因此当承诺解决时,您再次抛出承诺 - 无限循环。

根据 2.,即使您尝试将 promise 保存在 state 或 ref 等中,它仍然无法工作 - 无限循环。

因此,如果您想编写一些自定义挂钩,您确实需要使用任何 data-structure(可以全局管理{如您的 globalCache} 或由 React.Suspense 父级管理) 表示是否已抛出此特定 React.Suspense 的承诺(这正是 Relay 在 Facebook 的代码库中所做的)。

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

确实意味着父组件仍然要应对大量数据。第一个 render 可以用于启动 promise,但仍然必须 return 不抛出以避免无限循环。只有在第二次渲染时才会抛出实际暂停渲染的承诺。