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
:children
elements ofReact.Suspense
won't mount until the thrown promise resolved.useEffect
).Now, you throwing a
promise
from 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.Suspense
parent) which indicates if the promise from this specificReact.Suspense
has been thrown (thats exactly whatRelay
does in Facebook's codebase).