Skip to content

Commit 2f86b7c

Browse files
committed
Add docs for suspense and pending UI
1 parent 50b6d99 commit 2f86b7c

3 files changed

Lines changed: 186 additions & 1 deletion

File tree

versioned_docs/version-8.x/data-loading.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Loaders are not called on the initial render or for actions that update the curr
2222

2323
This is useful to implement "render-as-you-fetch" patterns, where the screen can suspend while reading data that the loader started fetching. If the screen is lazy loaded, the loader and the lazy loading of the screen run in parallel.
2424

25-
This pattern is designed to work with React's Suspense and error boundaries. You can provide a Suspense fallback and an error boundary for your screens using [`screenLayout`](navigator.md#screen-layout) or [`layout`](screen.md#layout) to show a loading UI while the screen is waiting for data or an error UI if the screen throws an error.
25+
This pattern is designed to work with React's Suspense and error boundaries. You can provide a Suspense fallback and an error boundary for your screens using [`screenLayout`](navigator.md#screen-layout) or [`layout`](screen.md#layout) to show a loading UI while the screen is waiting for data or an error UI if the screen throws an error. See [Suspense and pending UI](suspense.md) for more details on placing Suspense boundaries and handling pending UI during navigation.
2626

2727
## Adding a loader to a screen
2828

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
id: suspense
3+
title: Suspense and pending UI
4+
sidebar_label: Suspense and pending UI
5+
---
6+
7+
[React Suspense](https://react.dev/reference/react/Suspense) lets you show a fallback - the loading UI shown in place of content that isn't ready yet - while code or data for part of the UI is loading. In this guide, we'll cover how React Navigation works with Suspense and how to handle pending UI during navigation.
8+
9+
## What can suspend
10+
11+
A Suspense boundary shows its fallback when a component inside it suspends while rendering. A component suspends when it's loaded with [`React.lazy`](https://react.dev/reference/react/lazy), reads a cached Promise with [`React.use`](https://react.dev/reference/react/use), or reads data through a framework or library that integrates with Suspense.
12+
13+
Only data read through one of these Suspense-enabled sources can trigger a boundary. Data fetched in an Effect or event handler has to be routed through such a source first. The examples in this guide assume your code or data is loaded this way.
14+
15+
## How navigation behaves with Suspense
16+
17+
A navigation action refers to any action that updates the [navigation state](navigation-state.md), including actions that don't change the visible screen, such as updating params. Navigation actions triggered from a screen through APIs such as [`useNavigation`](use-navigation.md) or [`useLinkProps`](use-link-props.md) run as [React transitions](https://react.dev/reference/react/startTransition) (not to be confused with visual transition animations).
18+
19+
While the navigation transition is pending:
20+
21+
- The current screen stays visible and interactive.
22+
- Navigation and focus hooks and events still reflect the current screen.
23+
24+
Once the suspended content is ready, the destination appears and the navigator's animation starts. If you start another navigation before the pending one finishes, it takes over immediately, so you always land on the newest destination.
25+
26+
Some navigation actions don't use transitions, such as switching tabs in a [native tab navigator](bottom-tab-navigator.md), the native back action in a [native stack](native-stack-navigator.md), gesture-driven navigations etc. So a suspending destination shows the nearest fallback instead of keeping the current screen visible.
27+
28+
When writing a [custom navigator](custom-navigators.md), actions dispatched through the navigator's `navigation` prop don't automatically run as transitions. You can choose to wrap them in [`startTransition`](https://react.dev/reference/react/startTransition) based on how your navigator works.
29+
30+
## Existing and new Suspense boundaries
31+
32+
During a transition, React keeps already-visible content on screen instead of replacing it with a fallback. When a boundary first appears as part of the destination, it has no previous content to preserve, so it shows its fallback right away while the rest of the destination renders.
33+
34+
This is why a boundary around the navigator behaves differently from one inside a screen. The boundary around the navigator already exists, so it can keep the current screen visible. A new boundary inside the destination has nothing to keep visible, so it shows its loading UI instead.
35+
36+
## Choosing where to place Suspense boundaries
37+
38+
Where you place the nearest Suspense boundary decides what the user sees while content loads.
39+
40+
### Around the navigator
41+
42+
With a boundary around the whole navigator, the current screen stays visible while the destination suspends during a transition. You can use the navigator's [`layout`](navigator.md#layout) to add this boundary:
43+
44+
```js static2dynamic
45+
const RootStack = createNativeStackNavigator({
46+
// highlight-start
47+
layout: ({ children }) => (
48+
<React.Suspense fallback={<LoadingPlaceholder />}>
49+
{children}
50+
</React.Suspense>
51+
),
52+
// highlight-end
53+
screens: {
54+
Home: HomeScreen,
55+
Profile: ProfileScreen,
56+
},
57+
});
58+
```
59+
60+
This fallback is still shown during the initial render, when there is no previously displayed screen to keep visible, or if a component suspends because of a state update that isn't wrapped in a transition.
61+
62+
This can be useful for [stack navigators](stack-navigator.md), where you'd keep the current screen visible during navigation and [show pending UI on the current screen](#showing-pending-ui-on-the-current-screen).
63+
64+
It's not recommended for native tab navigators, where switching tabs by tapping the native tab bar doesn't run as a transition, so the fallback would hide the tab bar if a screen suspends.
65+
66+
### Around each screen
67+
68+
With a boundary around each screen, the destination appears immediately with its fallback instead of keeping the current screen visible. You can use the [`screenLayout`](navigator.md#screen-layout) prop to add a boundary to every screen in a navigator, or a screen's [`layout`](screen.md#layout) to add a boundary to just that screen:
69+
70+
```js static2dynamic
71+
const RootStack = createNativeStackNavigator({
72+
// highlight-start
73+
screenLayout: ({ children }) => (
74+
<React.Suspense fallback={<LoadingPlaceholder />}>
75+
{children}
76+
</React.Suspense>
77+
),
78+
// highlight-end
79+
screens: {
80+
Home: HomeScreen,
81+
Profile: ProfileScreen,
82+
},
83+
});
84+
```
85+
86+
This is ideal for tab and [drawer navigators](drawer-navigator.md), as it keeps the tab bar or drawer visible and shows the loading UI only in the screen's area.
87+
88+
### Around a section inside a screen
89+
90+
You don't always need to wait for the whole screen. If only specific parts of it suspend, you can wrap just that part in a boundary and let the rest of the screen render normally:
91+
92+
```js
93+
function ProfileScreen() {
94+
return (
95+
<>
96+
<ProfileHeader />
97+
// highlight-next-line
98+
<React.Suspense fallback={<FeedSkeleton />}>
99+
<ProfileFeed />
100+
</React.Suspense>
101+
</>
102+
);
103+
}
104+
```
105+
106+
In this example, `ProfileHeader` appears immediately while `FeedSkeleton` is shown in place of `ProfileFeed`. Once the feed is ready, it replaces the skeleton without affecting the header.
107+
108+
We recommend putting content that should appear together under the same boundary. Avoid adding separate boundaries around every component.
109+
110+
## Showing pending UI on the current screen
111+
112+
When the current screen stays visible after a navigation that suspends, it may not be obvious that tapping a button did anything. Showing a pending state on that button makes it clear that navigation is in progress.
113+
114+
You can use the [`useTransition`](https://react.dev/reference/react/useTransition) hook to show a loading indicator while the navigation transition is pending. We recommend encapsulating this in a reusable button component so that each control manages its own transition, and the loading state of one control doesn't affect the others.
115+
116+
For example, a button that navigates using [`useLinkProps`](use-link-props.md):
117+
118+
```js
119+
function LinkButton({ in: parent, screen, params, children }) {
120+
const { onPress, ...rest } = useLinkProps({ in: parent, screen, params });
121+
122+
// highlight-start
123+
const [isPending, startTransition] = React.useTransition();
124+
const isPendingDeferred = React.useDeferredValue(isPending);
125+
// highlight-end
126+
127+
return (
128+
<MyButton
129+
{...rest}
130+
// highlight-next-line
131+
loading={isPending && isPendingDeferred}
132+
onPress={(e) => {
133+
// highlight-start
134+
startTransition(() => {
135+
onPress(e);
136+
});
137+
// highlight-end
138+
}}
139+
>
140+
{children}
141+
</MyButton>
142+
);
143+
}
144+
```
145+
146+
In this example, deferring the `isPending` value with [`useDeferredValue`](https://react.dev/reference/react/useDeferredValue) helps avoid the loading indicator from flashing briefly when the navigation finishes quickly or the destination doesn't suspend.
147+
148+
You can then use the `LinkButton` for navigation:
149+
150+
```js
151+
<LinkButton screen="Profile" params={{ userId: 'jane' }}>
152+
Open profile
153+
</LinkButton>
154+
```
155+
156+
Similarly, you can wrap any navigation action in a transition and show a pending UI while the navigation transition is in progress.
157+
158+
If you have multiple controls that can start a navigation, we recommend using separate transitions for each control, so that the loading state of one doesn't affect the others.
159+
160+
If you use the [`Button`](elements.md#button) component from `@react-navigation/elements` for navigation, it automatically handles transitions and shows a loading indicator while the navigation is pending.
161+
162+
## Handling errors
163+
164+
Suspense boundaries only handle pending content. If loading fails, or the component throws during rendering, an [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) can be used to show an error message instead of crashing the whole app:
165+
166+
```js static2dynamic
167+
const RootStack = createNativeStackNavigator({
168+
screenLayout: ({ children }) => (
169+
// highlight-next-line
170+
<ErrorBoundary fallback={<ErrorFallback />}>
171+
<React.Suspense fallback={<LoadingPlaceholder />}>
172+
{children}
173+
</React.Suspense>
174+
</ErrorBoundary>
175+
),
176+
// highlight-end
177+
screens: {
178+
Home: HomeScreen,
179+
Profile: ProfileScreen,
180+
},
181+
});
182+
```
183+
184+
Typically, the error boundary should give the user a way to recover, such as retrying the failed action or navigating back to a safe screen.

versioned_sidebars/version-8.x-sidebars.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"icons",
4242
"state-persistence",
4343
"data-loading",
44+
"suspense",
4445
"web-support",
4546
"server-rendering"
4647
]

0 commit comments

Comments
 (0)