react-use-suspendable
v0.2.0
Published
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.
Readme
use-suspendable
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.
Migration from setting state in useEffect
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.
Old version:
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}>
}
</>
);
}New version w/ use ("react": ">=19.0.0"):
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>
);
}"react": ">=16.8 <19"
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.
What's the difference?
- 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
Other benefits
- Delegated loading state higher up the component tree (
MyComponentContainerin this case), allowing your component to handle just the rendering logic - If the promise errors, you're leaving it up to
Reactto decide how to handle it (i.e.ErrorBoundary) - In the first example, there was no clean up of the promise.
- Let's say
paramwas1then became2beforeexpensiveAsyncFunction(1)could complete. You'll callsetDatatwice, but you don't know in what order. - This is now handled by
React.
- Let's say
Main use cases
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.
