我正在尝试使用实验性的新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一起使用的信息?
答案 0 :(得分:1)
让我们回顾一下React.Suspense
上的一些事实:
children
元素要在抛出承诺得到解决之前才能安装。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),它有一个主要好处:没有数据的初始渲染永远不会发生。
这确实意味着父组件仍然必须处理大量数据。第一次渲染可用于启动承诺,但仍必须返回而不抛出以避免无限循环。只有在第二次渲染时才会抛出承诺,这实际上会暂停渲染。