As we build React applications, we create hierarchies of components by describing their structure through code - we tell React what to render. Sometimes these components need to access data through an API call and it becomes much harder to continue the same style of code - we can’t tell the components what the data is, but we can tell them how to get it. This is the difference between declarative and imperative styles of code, in which the former is embraced by React. What if we could not only write component hierarchies in a declarative style, but also get close to writing component definitions that rely on asynchronous operations in the same style? Let’s find out using React’s new Suspense API.
Engineers have used the lifecycle method componentDidMount (or useEffect with Hooks) to fetch data after a component has mounted for years. Conditional rendering is then used to render an interim loading state, often a component, if the data isn’t there yet, and the main component when the fetch is complete.
Giving a set of instructions to fetch data in a component definition presents two problems,
While these problems will never prevent a React application from functioning correctly, they do present an opportunity for engineers to come up with a solution that allows for better conformity to the declarative style of React as well as improving the maintainability of a codebase. With Concurrent Mode and Suspense, we can now write declarative asynchronous code in our React components while also being able to do any rendering on child components in the suspended parent component.
You can view the GitHub repository or CodeSandbox before we dive deep into the components.
IMPERATIVE DATA FETCHING
As mentioned previously, we normally have to give components a set of instructions for handling asynchronous operations - get this, set that, if this condition then render that. The convention for retrieving data includes fetching data in lifecycle methods, updating state when the data is resolved, and having some sort of conditional rendering that will handle whether or not the parent component has the data. While this certainly has worked for years, this approach has never adhered to one of React’s core principles - creating declarative components that describe structure rather than control flow.
Let’s first examine the fetch call implementations then take a look at the example application below using React Hooks in an imperative asynchronous style.
Fetch call implementation:
// api.js
export const fetchPokemon = () =>
fetch(`https://pokeapi.co/api/v2/pokemon/128`)
.then(res => res.json());
export const fetchEncounters = () =>
fetch("https://pokeapi.co/api/v2/pokemon/128/encounters")
.then(res => res.json());
Conditional rendering example:
// TraditionalComponent.js
import React from "react";
import { fetchPokemon, fetchEncounters } from "./api";
export const TraditionalComponent = () => {
const [pokemon, setPokemon] = React.useState(null);
React.useEffect(() => {
fetchPokemon().then(setPokemon);
}, []);
if (pokemon === null) {
return <p>Loading pokemon info...</p>;
}
return (
<React.Fragment>
<h1>{pokemon.species.name}</h1>
<img src={pokemon.sprites.front_default} alt="pokemon" />
<PokemonEncounters />
</React.Fragment>
);
};
export const PokemonEncounters = () => {
const [encounters, setEncounters] = React.useState(null);
React.useEffect(() => {
fetchEncounters().then(setEncounters);
}, []);
if (encounters === null) {
return <p>Loading encounters areas...</p>;
}
return (
<ul>
{encounters.map(encounter => (
<li key={encounter.location_area.name}>{encounter.location_area.name}</li>
))}
</ul>
);
};
The above example includes two component definitions, TraditionalComponent which conditionally renders a PokemonEncounter component that also has conditional rendering. Those instances of conditional rendering are used to figure out whether each of the components has completed its data fetching and set its state through useEffect (a React Hook similar to the componentDidMount lifecycle method).
Two notable concepts are the imperative fetch calls which then need to update state and the conditional rendering based upon the aforementioned state. For the latter, not only is it easy to imagine the confusion that might be caused by having multiple return statements, but more importantly, the data retrieval or rendering that is needed by the child component has to wait to begin its fetch or render process until the parent’s fetch call is resolved.
What if we could write these components that rely on asynchronous data in a declarative style and do render work on the child or sibling components while that parent is waiting for its data? Soon we will be able to achieve both in a production environment with React Concurrent Mode and Suspense.
ENTER CONCURRENT MODE
React’s Concurrent Mode allows us to move past blocking rendering to an environment of interruptible rendering. Interruptible rendering is the ability to do render work while being able to do another task, like updating state, at seemingly the same time. This article will not dive deep into how Concurrent Mode was implemented by the React team, though resources that deal with those issues will be provided at the end. For the sake of this blog post, we just need to know that with Concurrent Mode, we can achieve a state of rendering-as-we-fetch - fetching all the required data for the next screen as early as possible, and start rendering the new screen immediately — before we get a network response.
Concurrent Mode is still experimental and not to be used in production, but React has made it easy to incorporate into our projects with just a couple alterations to how we normally do things.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { TraditionalComponent } from "./TraditionalComponent";
ReactDOM.createRoot(document.getElementById("root")).render(
<TraditionalComponent />
);
Concurrent Mode is now enabled on your application and you are ready to refactor your imperative data fetching approach to declarative code with the added performance benefits of interruptible rendering.
DECLARATIVE DATA FETCHING WITH SUSPENSE
We previously walked through a scenario where components use conditional rendering to determine what is displayed on the screen dependent on whether the state has the fetched data or not. This was written imperatively, telling the component a list of instructions rather than merely describing its construction. The React team came up with a way of accomplishing data fetching with a much more declarative style.
React’s Suspense API enables a developer to declaratively write a component that relies on asynchronous code or data and render a fallback in the meantime. On a technical level, whatever component is relying on asynchronous data has to throw a Promise that is then caught by a Suspense component. This process helps developers create code that adheres to React’s core principle of declarative component architecture. Let’s take a look at a refactored version of the first data fetching example.
Declarative component with Suspense example:
// SuspenseComponent.js
import React, { Suspense } from "react";
import { pokemonResource, encountersResource } from './PromiseWrapper';
export const SuspenseComponent = () => {
return (
<React.Fragment>
<Suspense fallback={<h1>Loading pokemon data...</h1>}>
<PokemonDetails />
<Suspense fallback={<h1>Loading area encounters...</h1>}>
<EncountersList />
</Suspense>
</Suspense>
</ React.Fragment>
);
};
function PokemonDetails() {
const pokemon = pokemonResource.read();
return (
<React.Fragment>
<h1>{pokemon.species.name}</h1>
<img src={pokemon.sprites.front_default} alt="pokemon" />
</React.Fragment>
);
}
function EncountersList() {
const encounters = encountersResource.read();
return (
<ul>
{encounters.map(encounter => (
<li key={encounter.location_area.name}>
{encounter.location_area.name}
</li>
))}
</ul>
);
}
Updated root component rendering SuspenseComponent:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { SuspenseComponent } from "./SuspenseComponent";
ReactDOM.createRoot(document.getElementById("root")).render(
<SuspenseComponent />
);
This new approach brings about a couple important features,
It is also important to observe that while the top level Suspense component caught a Promise from the first inner component, React in Concurrent Mode will be able to continue render work on the rest of the tree (including the other Suspense component and its internal component) while rendering a fallback during the meantime. This is one of the core features of Concurrent Mode, enabling us to partially render trees, waiting for some child components to resolve their asynchronous operations while beginning render work on other child components, seemingly at the same time.
While the example above showcases incredibly simple, declarative code for the slightly complex operation of rendering asynchronous data, there will inevitably questions that surround the aforementioned resource API and its read method.
CUSTOM PROMISE WRAPPERS
React, and its new Suspense API, is only worried about rendering views through declarative code and holds no solution for the actual fetching of code or data by definition. At the time of writing, and possibly a lot longer, developers will have to come up with solutions that not only retrieve data but also conform those implementations to the specifications and expectations of the Suspense API - mainly throwing Promises then returning data. Below we will examine a custom Promise wrapper crafted to work with Suspense.
// PromiseWrapper.js
import { fetchPokemon, fetchEncounters } from "./api";
function wrapPromise(promise) {
let status = "pending";
let result;
let suspender = promise.then(
r => {
status = "success";
result = r;
},
e => {
status = "error";
result = e;
}
);
return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
}
};
}
const pokemonResource = wrapPromise(fetchPokemon());
const encountersResource = wrapPromise(fetchEncounters());
We create a function wrapPromise that accepts a Promise and returns an object that has a read method, which has access to the properties status, result, and suspender via closure. The status keeps track of instantaneous state of the Promise lifecycle, whether it’s pending, rejected, or resolved, and the result property stores a reference to the return value of the Promise’s rejection or resolution. Finally, the suspender property attaches a then method to the Promise, changing the value of status and result upon rejection or resolution. When the fetch calls are invoked, they will return a Promise as the argument to the invocation of wrapPromise that return a resource object. In this implementation, the resource object will throw the Promise when read is invoked, which is ultimately caught by a Suspense component.
As time progresses, developers will flesh out data fetching libraries to be used with Suspense - one current example is Relay, which works with GraphQL requests. Most importantly, you can write a function like wrapPromise once and use it to interface with the Suspense API for any number of calls to a variety of endpoints. In the end, implementing a Promise wrapper might take a little more development time than we wish, but also gets us closer to understanding just how the Suspense API works internally, and subsequently how we can write declarative component definitions that rely on asynchronous operations.
CONCLUSION
Writing components that rely on asynchronous operations in React is nothing new, but composing them with declarative code is. The Suspense API helps cleanly organize our component structure and all the developer has to worry about is the implementation of the data fetching. While using Suspense for lazily loaded components was released in the same version of React that Hooks was, Suspense for data fetching and Concurrent Mode are still experimental and their API’s are subject to change - though the principle concept of a Suspense component catching a thrown Promise and rendering a fallback until the Promise is resolved will not. As tentative as the features may seem, writing asynchronous components with declarative code will become a core principle of React when these API’s are finalized.
ADDITIONAL RESOURCES