|
| 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 | +Suspense boundaries respond when a child suspends while rendering. This includes components loaded with [`React.lazy`](https://react.dev/reference/react/lazy), cached Promises read with [`React.use`](https://react.dev/reference/react/use), and data read through a framework or library that integrates with Suspense. |
| 12 | + |
| 13 | +Data requested in an Effect or event handler needs to be read through a Suspense-enabled data source before a boundary can respond to it. The examples in this guide assume that code or data is loaded through one of the Suspense-enabled approaches above. |
| 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 continues showing content that's already visible. A boundary rendered with the destination for the first time can still show its fallback immediately while the rest of the destination appears. |
| 33 | + |
| 34 | +This is why a boundary around the navigator and a boundary inside a screen can behave differently. The boundary around the navigator can keep the current screen visible, while a new boundary inside the destination can show its loading UI. |
| 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 | + <React.Suspense fallback={<FeedSkeleton />}> |
| 98 | + <ProfileFeed /> |
| 99 | + </React.Suspense> |
| 100 | + </> |
| 101 | + ); |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +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. |
| 106 | + |
| 107 | +We recommend putting content that should appear together under the same boundary. Avoid adding separate boundaries around every component. |
| 108 | + |
| 109 | +## Showing pending UI on the current screen |
| 110 | + |
| 111 | +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. |
| 112 | + |
| 113 | +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. |
| 114 | + |
| 115 | +For example, a button that navigates using [`useLinkProps`](use-link-props.md): |
| 116 | + |
| 117 | +```js |
| 118 | +function LinkButton({ in: parent, screen, params, children }) { |
| 119 | + const { onPress, ...rest } = useLinkProps({ in: parent, screen, params }); |
| 120 | + |
| 121 | + // highlight-start |
| 122 | + const [isPending, startTransition] = React.useTransition(); |
| 123 | + const isPendingDeferred = React.useDeferredValue(isPending); |
| 124 | + // highlight-end |
| 125 | + |
| 126 | + return ( |
| 127 | + <MyButton |
| 128 | + {...rest} |
| 129 | + // highlight-next-line |
| 130 | + loading={isPending && isPendingDeferred} |
| 131 | + onPress={(e) => { |
| 132 | + // highlight-start |
| 133 | + startTransition(() => { |
| 134 | + onPress(e); |
| 135 | + }); |
| 136 | + // highlight-end |
| 137 | + }} |
| 138 | + > |
| 139 | + {children} |
| 140 | + </MyButton> |
| 141 | + ); |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +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. |
| 146 | + |
| 147 | +You can then use the `LinkButton` for navigation: |
| 148 | + |
| 149 | +```js |
| 150 | +<LinkButton screen="Profile" params={{ userId: 'jane' }}> |
| 151 | + Open profile |
| 152 | +</LinkButton> |
| 153 | +``` |
| 154 | + |
| 155 | +Similarly, you can wrap any navigation action in a transition and show a pending UI while the navigation transition is in progress. |
| 156 | + |
| 157 | +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. |
| 158 | + |
| 159 | +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. |
| 160 | + |
| 161 | +## Handling errors |
| 162 | + |
| 163 | +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: |
| 164 | + |
| 165 | +```js static2dynamic |
| 166 | +const RootStack = createNativeStackNavigator({ |
| 167 | + screenLayout: ({ children }) => ( |
| 168 | + // highlight-next-line |
| 169 | + <ErrorBoundary fallback={<ErrorFallback />}> |
| 170 | + <React.Suspense fallback={<LoadingPlaceholder />}> |
| 171 | + {children} |
| 172 | + </React.Suspense> |
| 173 | + </ErrorBoundary> |
| 174 | + ), |
| 175 | + // highlight-end |
| 176 | + screens: { |
| 177 | + Home: HomeScreen, |
| 178 | + Profile: ProfileScreen, |
| 179 | + }, |
| 180 | +}); |
| 181 | +``` |
| 182 | + |
| 183 | +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. |
0 commit comments