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:
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.
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.
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 andReact.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;
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>;
};
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;
}
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;
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)
Great article! nobody explains how to use <Suspense /> without a library, your article was the only resource that I could find!
Thanks! Glad it could help