Suspense and use()
Read a Promise during render with use(), and let a Suspense boundary show a fallback while the data loads.
On Day 4 you fetched data in an Effect. It worked, but every component needed the same three pieces of state: data, isLoading and error. You also had to write cleanup code so that an old response couldn't overwrite a new one.
React 19 gives you a different way to wait for data. A component asks for the data with use(). If the data isn't ready yet, the component suspends, which means React pauses rendering it and shows a fallback that you chose. When the data arrives, React renders the component again, and this time the data is there.
Your first suspending component
Here is a quote of the day that takes one and a half seconds to "load". Read it from top to bottom, then press the refresh button in the preview to watch the loading state again.
Look at what's missing from Quote. There's no useState, no useEffect and no if (isLoading). The component is written as if the data were already there. Two pieces work together:
use(promise)returns the resolved value of a Promise. If the Promise is still pending,usesuspends the component.<Suspense fallback={...}>is a boundary. When anything inside it suspends, React shows thefallbackin place of the whole boundary's content. When everything inside is ready, React swaps the real content in.
use is not quite a Hook. It starts with "use" and you still call it only inside components and Hooks, but unlike Hooks you're allowed to call it inside an if or a loop. You'll see why that's useful at the end of this lesson.
The Promise must be cached
Look again at api.js. fetchQuote creates the Promise once and hands back the same one on every call. That isn't an accident.
When a component suspends, React throws away that render. Later it calls your component function again from the top. If your component creates a new Promise each time it renders, the new Promise is pending too, so the component suspends again, and again, forever.
A real app needs different data for different ids, so the cache becomes a Map from id to Promise. Switch between the books below. The first visit to each book shows the fallback. The second visit is instant, because the Promise is already resolved and cached.
Notice that the old book disappears and the fallback replaces it while the new book loads. Sometimes that's what you want. Often you'd rather keep the old book on screen, slightly dimmed, until the new one is ready. That's what Transitions do, and you'll learn them two lessons from now.
Deep diveWhat can make a component suspend?
Only a few things trigger a Suspense boundary:
- reading a Promise with
use(); - loading code with
lazy()(a component whose code is downloaded on demand); - data fetching in frameworks that support Suspense, such as Next.js, or libraries like TanStack Query when you use their Suspense hooks.
Fetching data inside a useEffect does not trigger Suspense. The Effect runs after the component has already rendered, so there's nothing for Suspense to wait for. That's why the Day 4 fetching code needed its own isLoading state.
In real apps you rarely write the cache by hand. Frameworks and data libraries keep it for you. Writing one yourself once, as you did above, is the best way to understand what they're doing.
Nesting boundaries and handling errors
You decide where the loading states go by where you put <Suspense>. A boundary shows its fallback if anything inside it suspends. So if two components should appear together, put them in the same boundary. If one is slower and shouldn't hold the other back, give it its own boundary.
If the Promise rejects (fails), use throws the error. React passes it up to the nearest error boundary, the component you met on Day 5 that catches errors and shows a fallback. In this sandbox, the reviews fail about half the time. Press the refresh button in the preview a few times.
The book shows after 0.8 seconds, and it doesn't wait for the reviews, because the reviews have their own inner boundary. If the reviews fail, only that small part of the page shows an error. The rest of the page keeps working.
Why react-error-boundary here?
On Day 5 you wrote your own ErrorBoundary. To keep these sandboxes short, they use the small react-error-boundary package, which works the same way. In your project, use the one you already have.
A component calls use(fetchPosts()) and keeps showing its Suspense fallback forever. What's the most likely cause?
Reading context with use()
use can also read context. use(ThemeContext) does the same job as useContext(ThemeContext), with one difference: you may call it inside a condition. That lets you skip reading context you don't need.
With useContext, that early return before the Hook call would break the Rules of Hooks. With use, it's fine. You'll see use(SomeContext) in modern code, including the Reading List's BooksContext today.
Make the Reading List detail suspend
BookDetail below still fetches in an Effect with isLoading state, as on Day 4. Rewrite it so that:
BookDetailreads the details withuse(fetchBookDetails(id));Appwraps it in<Suspense>with the fallback "Loading details...";- there's no
useEffectorisLoadingleft.
fetchBookDetails in api.js already caches its Promises.
Show solution
import { Suspense, use, useState } from 'react';
import { fetchBookDetails } from './api.js';
export default function App() {
const [id, setId] = useState('1');
return (
<main>
<button onClick={() => setId('1')}>The Hobbit</button>{' '}
<button onClick={() => setId('2')}>Dune</button>
<Suspense fallback={<p>Loading details...</p>}>
<BookDetail id={id} />
</Suspense>
</main>
);
}
function BookDetail({ id }) {
const details = use(fetchBookDetails(id));
return (
<article>
<h2>{details.title}</h2>
<p>{details.summary}</p>
</article>
);
}The component shrinks from about 20 lines to 7. You no longer need the ignore flag, because React always renders with the id from the latest props. A slow response for an old id can't overwrite the new one. The loading state moved out of the component and into the boundary, where the parent decides how it looks.
Which of these does NOT cause a nearby <Suspense> boundary to show its fallback?
Day 6 progress
Day 6: Modern React 19
Suspense and use(), form Actions, optimistic UI, transitions, animated view transitions, the React Compiler and testing.
Actions and forms
Submit forms with Actions, show pending and error states with useFormStatus and useActionState, and update the UI instantly with useOptimistic.