In the current version of React (17.0.2
at the day of this article creation) Suspense
is a component, that allows developers to lazy-load application parts. It accepts fallback
property, with content to display, while the child component is lazy-loading.
const SomePage = React.lazy(() => import("./modules/somepage"));
const App () => (
<React.Suspense fallback={"Loading..."}>
<SomePage />
</React.Suspense>
);
However from React 18 it will be possible to use Suspense
for data fetching. This means, that fallback
will be displayed until component will fetch all the data needed. Or in general all events that component expect will occur. Let say we want to just display placeholder for 2 seconds:
const Main = () => {
useTimeout(2000);
return <div>Component loaded</div>;
};
const App = () => (
<Suspense fallback={"Loading..."}>
<Main />
</Suspense>
);
export default App;
As you could guess, Loading...
will be displayed for 2 seconds and Component loaded
afterwards.
However when I first saw the code above, I didn't understand HOW did that happen.. What is that magic mechanism in the useTimeout
hook? In short it has to:
- Stop component code execution.
- Let
Suspense
know that the component isn't yet ready - Notify
Suspence
know when it should re-attempt with rendering component.
To stop code execution you need use throw
statement. In order to make Suspense
know it's expected, the value thrown need to be a Promise
. Suspense
will catch this promise and subscribe to it, to re-attempt rendering.
Please note: the code bellow is just for a demo purpose:
let fullfilled = false;
let promise = null;
const useTimeout = (ms: number) => {
// check if timeout already occurred.
if (!fullfilled) {
// if promise doesn't exist create and throw it.
throw promise ||= new Promise((res) => {
setTimeout(() => {
// on next attempt consider timeout completed.
fullfilled = true;
// resolve promise (will ask react to re-render).
res();
}, ms);
});
}
};
(Confused about ||=
? Check this doc)
It turns out that suspense uses quite simple mechanisms, but there's a hard part. You might ask why fullfilled
and promise
couldn't be stored in a ref, so the hook would be reusable:
const fullfilled = useRef(false);
It turns out, that while component is not loaded, hooks can't be really used. Component will be unmounted / mounted on every render attempt before the render will complete without throwing promises. Hence to figure out, if this component has actually started data loading process, we should rely on a globally available cache. (In our simplified case it's just fullfilled
variable). Of course in a real-world example such simple approach wouldn't work (this hook works only one time).
This is why it's advised to use good libraries that supports suspense (like swr).
Full code of the example above.
👋
Top comments (0)