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 一起工作的内容?
让我们回顾一下关于 React.Suspense
的一些事实:
React.Suspense
的 children
元素在 thrown promise 解决之前不会挂载。
- 您必须从函数体中抛出承诺(而不是像
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 不抛出以避免无限循环。只有在第二次渲染时才会抛出实际暂停渲染的承诺。
我正在尝试使用实验性的新 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 一起工作的内容?
让我们回顾一下关于 React.Suspense
的一些事实:
React.Suspense
的children
元素在 thrown promise 解决之前不会挂载。- 您必须从函数体中抛出承诺(而不是像
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 不抛出以避免无限循环。只有在第二次渲染时才会抛出实际暂停渲染的承诺。