DEV Community

vaukalak
vaukalak

Posted on

React 18 Suspense minimal example

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>
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

  1. Stop component code execution.
  2. Let Suspense know that the component isn't yet ready
  3. 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);
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

(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);
Enter fullscreen mode Exit fullscreen mode

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)