DEV Community

TusharShahi
TusharShahi

Posted on • Updated on

Using Suspense with React: without a 3rd party library

The basics

Some time ago, React came out with Suspense and has changed the way data fetching is done in React-powered applications.

From a fetch-then-render or a fetch-on-render strategy, we are moving to a render-as-you-fetch strategy.
There are multiple blogs explaining the difference between the three but here is a quick TLDR:

  1. fetch-on-render - Most common. This is where we let a component render (a loading UI) and in effect, we make our call to fetch the data.

  2. fetch-then-render - We fetch all the data first and then render the component sub-tree. Again we use an effect here but try to render at least the children after their data is already loaded in the parent.

  3. fetch-as-you-render - Used along with React.Suspense, the data call is made while the component is being rendered. It is not inside an effect. While the data is being loaded the component is in a suspended state and React.Suspense is used to show a fallback UI.

How to use it?

According to the React docs, ideally, a developer should not have to interact with the Suspense API themself. The developer should make use of certain libraries that the React team recommends and that are compatible with the Suspense framework.

From the docs:

Suspense-enabled data fetching without the use of an opinionated framework is not yet supported. The requirements for implementing a Suspense-enabled data source are unstable and undocumented. An official API for integrating data sources with Suspense will be released in a future version of React.

A few examples are React-Query, Next.Js. SWR.

If we use something like React Query inside a Suspense tree we should be able to take advantage of Suspense features.

A working demo: To throw a promise

There are only a few docs to help with working with Suspense directly. I tried creating a working example using my basic understanding of what might be happening under the hood.

Suspense does not detect when data is fetched inside an Effect or event handler. One of the major ideas of working with Suspense is that data fetching should not be done in effect, but while rendering itself. This means data should be fetched inside the body of the React component.

So ideally, this is how fetching data might look like:

import React, { useEffect, useState, Suspense } from "react";

const Todo = ({ id }) => {
  const data = fetchSomeData(
    `https://jsonplaceholder.typicode.com/todos/${id}?_delay=2000`
  );
  return <p>{data.title}</p>;
};

function App() {
  return (
    <div>
      <Suspense fallback={<p>...loading</p>}>
        <Todo id={1} />
        <Todo id={2} />
        <Todo id={3} />
      </Suspense>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

A glance at the code tells us that fetchSomeData should return our fetched data. But it also tells us that the above code will fail because data will be filled asynchronously and data.title will throw a ReferenceError.

This is where Suspense comes into the picture. Our component subtree should let Suspense know it is in a Suspended state and that the fallback UI: <p>...loading</p> should be shown.

To check this out, one can throw a promise and the UI will Suspend. Here is a demo for the same.

Now since there is no wrapping Suspense boundary, there was no fallback handling and the app crashed.

Along similar lines, we can get our Suspense to work by creating fetchSomeData to work so that it throws a promise while it is not ready (indicating that it is in a suspended state).

Our basic structure should look like the below (Don't use it):

function fetchWithSuspense(url) {
  const promise = fetch(url)
    .then((response) => {
      return response.json();
    })
    .then((data) => {
      return data;
    });

  throw promise;
}

const Todo = ({ id }) => {
  const data = fetchWithSuspense(
    `https://jsonplaceholder.typicode.com/todos/${id}?_delay=2000`
  );

  return <p>{data.title}</p>;
};
Enter fullscreen mode Exit fullscreen mode

The problem with the above is that it will trigger a million network requests as we are never returning data correctly. We are only throwing our promise and because this time we have a Suspense boundary enabled our component is being rendered again. This calls fetchWithSuspense again and the cycle goes on.

Preventing unnecessary calls

To prevent the above, we can store the result of our network calls when the promise is fulfilled. This helps us return early from fetchWithSuspense once we have the data.

Our final method definition looks like this:

let cacheMap = {};
function fetchWithSuspense(url) {
  if (cacheMap[url]) return cacheMap[url];

  const promise = fetch(url)
    .then((response) => {
      return response.json();
    })
    .then((data) => {
      cacheMap = { ...cacheMap, [url]: data };
      return data;
    });

  throw promise;
}
Enter fullscreen mode Exit fullscreen mode

We created a simple cache map above and then used it to return the data once it is fetched and parsed correctly.

Putting the above all together we have something like:

import React, { useEffect, useState, Suspense } from "react";

let cacheMap = {};
function fetchWithSuspense(url) {
  if (cacheMap[url]) return cacheMap[url];

  const promise = fetch(url)
    .then((response) => {
      return response.json();
    })
    .then((data) => {
      cacheMap = { ...cacheMap, [url]: data };
      return data;
    });

  throw promise;
}

const Todo = ({ id }) => {
  const data = fetchWithSuspense(
    `https://jsonplaceholder.typicode.com/todos/${id}?_delay=2000`
  );

  return <p>{data.title}</p>;
};

function App() {
  return (
    <div>
      <Suspense fallback={<p>...loading</p>}>
        <Todo id={1} />
        <Todo id={2} />
        <Todo id={3} />
      </Suspense>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Here is a working demo

Notice how we see the fallback ...loading first and then the three titles. All this without a million network calls.

Summary

Above, we saw a small demo of how suspense-enabled libraries might be working under the hood. Reality might be a lot different and the internals are much more complex and handle a lot more cases than my simple demo. Nonetheless, I hope the blog post gave you a basic idea of how libraries like SWR, react-query, and event hooks like use might be integrating with React.Suspense under the hood.

Top comments (2)

Collapse
 
abiria profile image
Abiria • Edited

Great article! nobody explains how to use <Suspense /> without a library, your article was the only resource that I could find!

Collapse
 
tusharshahi profile image
TusharShahi

Thanks! Glad it could help