React18 brings Suspense as a powerful component for coding

Today our Top-senior referent Pia Rovira is shedding light on React18 and Suspense component.

It’s a React component (available from React 18) that lets you display a fallback declaratively until its children have finished loading.

It’s not a data fetching library, nor is it a way to manage state, it just lets your components communicate to React that they are waiting for some data.

Suspense does not detect when data is fetched inside an Effect or event handler.

Suspense caveat

⚠️ Only Suspense-enabled data sources will activate the Suspense component. Suspense is kind of new, so not every data-fetching library is integrated to it yet. Suspense enabled data sources include:

  • Data fetching with Suspense-enabled frameworks/libraries
  • Lazy-loading component code with lazy
  • Reading the value of a Promise with use
Props for suspense

Suspense component receives 2 props.

  • children: The actual UI you intend to render.
  • fallback: The component to be rendered while children is still loading.

Example

With Suspense we can go from:

const App = () => {
  const [userDetails, setUserDetails] = useState({})

  useEffect(() => {
    fetchUserDetails().then(setUserDetails)
  }, [])

  if (!userDetails.id) return <p>Fetching user details...</p>

  return (
    <div className="app">
      <h2>Simple Todo</h2>

      <UserWelcome user={userDetails} />
      <Todos />
    </div>
  )
}

to:

const data = fetchData() // this method should be integrated with Suspense

const App = () => (
    <Suspense fallback={<p>Fetching user details...</p>}>
      <UserWelcome />
    </Suspense>
)

const UserWelcome = () => {
  const userDetails = data.userDetails.read();
	
	return <div>Hello {userDetails.name}!</div>
}

Code is cleaner and userDetails is always defined. While data is loading, we will see <p>Fetching user details...</p> and then, when data promise resolves we will see the UserWelcome component.

We can also nest Suspense components, cause suspense does not suspend 🤯. For example:

<Suspense fallback={<BigSpinner />}>
  <Biography />
  <Suspense fallback={<AlbumsGlimmer />}>
    <Panel>
      <Albums />
    </Panel>
  </Suspense>
</Suspense>

In this case, At first we will see the BigSpinner while Biography data is being fetched. Then, we will see Biography component and the AlbumsGlimmer component while the albums data is loading.

Lastly, when having biography data and albums data, we will see Biography and Albums components.

So, just to see the evolution of our components:

1. Everything is loading
<BigSpinner />

2. Biography data already loaded, albums data is still fetching
<>
	<Biography />
	<AlbumsGlimmer />
</>

3. Biography data and albums data fetched
<>
	<Biography />
	<Panel>
	  <Albums />
  </Panel>
</>

React-query example in React18

React-query is one of the libraries that has hooks that integrate with Suspense on the latest version v5.

I’ll add an example below that includes Suspense and Error handling.

import React, { Suspense } from 'react';
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

import { Cards, Footer, Header, Main, Wrapper } from 'components';
import GlobalStyle from 'styles/globalStyles';

const Loading = () => <div>Loading...</div>

const ErrorView = ({ error, resetErrorBoundary }: FallbackProps) => {
	return (
		<div>
			<div>{error.message}</div>
			<button title='Retry' onClick={resetErrorBoundary} />
		</div>
	);
};

const Home: React.FC = () => {
	return (
		<Wrapper>
			<>
				<GlobalStyle />
				<Header />
				<QueryErrorResetBoundary>
					{({ reset }) => (
						<ErrorBoundary onReset={reset} FallbackComponent={ErrorView}>
							<Suspense fallback={<Loading />}>
								<Main />
							</Suspense>
						</ErrorBoundary>
					)}
				</QueryErrorResetBoundary>
				<Cards />
				<Footer />
			</>
		</Wrapper>
	);
};

export default Home;

And the Main component:

import React from 'react';

import { Button } from 'components';
import { useSuspenseQuery } from '@tanstack/react-query';
import { getData } from 'services/api';

export const Main: React.FC = () => {
	const queryResponseRQ = useSuspenseQuery({
		queryKey: ['deutschland'],
		queryFn: () => getData('name/deutschland'),
	});

	return (
		<div
			style={{
				display: 'flex',
				flexDirection: 'column',
				justifyContent: 'center',
				alignItems: 'center',
				textAlign: 'center',
				paddingTop: 32,
				paddingBottom: 32,
			}}>
			<div>{queryResponseRQ.data.data[0].name.common}</div>
		</div>
	);
};

Good news here is that errors and loading states are handled by Suspense and ErrorBoundary and so when Main component displays the query Response data is always defined.

No need to using ? or verifying to check nothing crashes.

Suspense List

<SuspenseList> coordinates the “reveal order” of the closest <Suspense> nodes below it.

 <code>❗ Still not available in React 18. But will be included in a future release.</code>

Suspense Props

  • revealOrder: can be "forwards" , "backwards" or "together". And it is the prop that indicates how the content will be revealed
  • tail: controls how many loading states are visible at once.

Example

function ProfilePage({ resource }) {
  return (
    <SuspenseList revealOrder="forwards">
      <ProfileDetails resource={resource} />
      <Suspense fallback={<h2>Loading posts...</h2>}>        
				<ProfileTimeline resource={resource} />      
			</Suspense>      
			<Suspense fallback={<h2>Loading fun facts...</h2>}>        
				<ProfileTrivia resource={resource} />      
			</Suspense>    
		</SuspenseList>  
	);
}

Rounding up Suspense!

Keep on exploring our blog post about React and React Native Improved Architecture. Some more tips are coming up…

Check out our other posts for more insights!