This is a hook to create a "suspendable" -- a promise cached to the current element's structure, in such a way that when the component gets destroyed and recreated, it will be preserved.
This is useful when using it to suspend.
[!WARNING]
This was designed to solve the problem where async requests are made in effects, not other uses of async with React.
Otherwise known as a cascading render, setting state in an effect is bad behavior, because effects are activated by... state updates.
Read more about this in the react docs.
import {useEffect, useState} from 'react';
function MyComponent({param}) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(()=>{
expensiveAsyncFunction(param)
.then((result)=>{
setData(result);
})
.finally(()=>{
setIsLoading(false);
})
}, [param]);
// more hook code here
return (
<>
{
isLoading ?
<LoadingState /> :
<DataRenderer data={data}>
}
</>
);
}
import {use} from 'react';
import useSuspendable from 'react-use-suspendable';
function MyComponent({param, ...passThroughProps}) {
const [promise] = useSuspendable(
()=>expensiveAsyncFunction(param),
[param]
);
const data = use(promise);
// more hook code here
return (
<DataRenderer
data={data}
{...passThroughProps}
/>
);
}
function MyComponentContainer(props) {
return (
<Suspense fallback={<LoadingState />}>
<MyComponent {...props} />
</Suspense>
);
}
You can use use-suspendable/map-promise
or use-suspendable/wrap-promise
or another promise synchronizer such as p-state.
import useSuspendable from 'react-use-suspendable';
import wrapPromise, {
getValue,
getReason,
isDone,
isRejected,
} from 'react-use-suspendable/wrap-promise';
function MyComponent({param, ...passThroughProps}) {
const [promise] = useSuspendable(
()=>wrapPromise(expensiveAsyncFunction(param)),
[param]
);
if (isRejected(madePromise)) {
throw getReason(madePromise); // throwing the error
}
if (!isDone(madePromise)) {
throw madePromise;
}
const data = getValue(madePromise);
// more hook code here
return (
<DataRenderer
data={data}
{...passThroughProps}
/>
);
}
function MyComponentContainer(props) {
return (
<Suspense fallback={<LoadingState />}>
<MyComponent {...props} />
</Suspense>
);
}
use-suspendable/wrap-promise
and use-suspendable/map-promise
have the same API, but work different internally -- wrap-promise
will modify the promise to store its state while map-promise
will store the promise in a WeakMap
at the module-level.
- In the old version, we:
- rendered a loading state
- waited for first paint
- started fetching data
- once the data was fetched, we updated state
- In the new version, we:
- immediately started fetching data without waiting for a paint
- suspended while that data fetch had already started
- painted when the data came back
- Delegated loading state higher up the component tree (
MyComponentContainer
in this case), allowing your component to handle just the rendering logic - If the promise errors, you're leaving it up to
React
to decide how to handle it (i.e.ErrorBoundary
) - In the first example, there was no clean up of the promise.
- Let's say
param
was1
then became2
beforeexpensiveAsyncFunction(1)
could complete. You'll callsetData
twice, but you don't know in what order. - This is now handled by
React
.
- Let's say
When you have something asynchronous you need to fetch because of a state change, i.e. a search query.
Otherwise, you're better off caching the promise at the module-level.