I'm trying to use the experimental new React feature Suspense for data fetching.
Here's my simple useApi hook which (if I understand Suspense correctly) either returns the result of an fetch call or throws the suspender promise. (slightly modified 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;
}
I'm using this hook simply like this:
function Child({ path }) {
const data = useApi(path);
return "ok";
}
export default function App() {
return (
<Suspense fallback="Loading…">
<Child path="/some-path" />
</Suspense>
);
}
It never resolves.
I think the problem is that useRef isn't quite working as it's supposed to.
If I initialize the ref with a random value, it doesn't retain that value, and instead gets reinitialized with another random value:
const ref = React.useRef({ time: +new Date() });
console.log(ref.current.time)
1602067347386
1602067348447
1602067349822
1602067350895
...
There's something weird about throwing the suspender that causes the useRef to reinitialize on every call.
throw ref.current.suspender;
If I remove that line useRef works as intended, but obviously Suspense doesn't work.
Another way I can make it work is if I use some sort of custom caching outside of React, like:
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;
}
This also makes it work, but I would rather use something that React itself provides in terms of caching component-specific data.
Am I missing something on how useRef is supposed to, or not supposed to work with Suspense?
Let's review some facts on
React.Suspense:childrenelements ofReact.Suspensewon't mount until the thrown promise resolved.useEffect).Now, you throwing a
promisefrom your custom hook, but according to1.the component never mounts, so when the promised resolves, you throwing the promise again - infinite loop.According to
2., even if you try saving the promise in a state or ref etc. still it wont work - infinite loop.Therefore, if you want to write some custom hook, you indeed need to use any data-structure (can be managed globally {like your
globalCache} or byReact.Suspenseparent) which indicates if the promise from this specificReact.Suspensehas been thrown (thats exactly whatRelaydoes in Facebook's codebase).