|
1 | 1 | --- |
2 | 2 | id: concurrent |
3 | | -title: Concurrent React/React Suspense |
| 3 | +title: Concurrent React |
4 | 4 | --- |
5 | 5 |
|
6 | | -_Not written yet._ watch [https://github.com/sw-yx/fresh-async-react](https://github.com/sw-yx/fresh-async-react) for more on React Suspense and Time Slicing. |
| 6 | +The Concurrent React APIs (`Suspense`, `useTransition`, `useDeferredValue`, `startTransition`, `use`) let you keep the UI responsive while React renders work in the background or waits for data. They're all stable as of React 18 and gained additional capabilities in React 19. |
| 7 | + |
| 8 | +## `Suspense` |
| 9 | + |
| 10 | +`Suspense` lets you declaratively show a fallback while a child component is waiting for something — typically data unwrapped with `use(promise)`, a lazy component, or a streamed boundary on the server. |
| 11 | + |
| 12 | +```tsx |
| 13 | +import { Suspense } from "react"; |
| 14 | + |
| 15 | +const UserProfile = ({ userPromise }: { userPromise: Promise<User> }) => { |
| 16 | + const user = use(userPromise); |
| 17 | + return <p>Hello, {user.name}!</p>; |
| 18 | +}; |
| 19 | + |
| 20 | +const App = ({ userPromise }: { userPromise: Promise<User> }) => ( |
| 21 | + <Suspense fallback={<p>Loading...</p>}> |
| 22 | + <UserProfile userPromise={userPromise} /> |
| 23 | + </Suspense> |
| 24 | +); |
| 25 | +``` |
| 26 | + |
| 27 | +`SuspenseProps` is typed as `{ children?: ReactNode; fallback?: ReactNode }`. The fallback can be any `ReactNode`, including `null`. |
| 28 | + |
| 29 | +## `use` |
| 30 | + |
| 31 | +`use` reads the value of a context or a promise. Unlike `useContext`, it can be called inside conditions and loops, and it integrates with `Suspense` for promises. |
| 32 | + |
| 33 | +```tsx |
| 34 | +import { use } from "react"; |
| 35 | + |
| 36 | +const Comments = ({ |
| 37 | + commentsPromise, |
| 38 | +}: { |
| 39 | + commentsPromise: Promise<Comment[]>; |
| 40 | +}) => { |
| 41 | + // Suspends until the promise resolves; throws to the nearest <Suspense>. |
| 42 | + const comments = use(commentsPromise); |
| 43 | + return ( |
| 44 | + <ul> |
| 45 | + {comments.map((c) => ( |
| 46 | + <li key={c.id}>{c.text}</li> |
| 47 | + ))} |
| 48 | + </ul> |
| 49 | + ); |
| 50 | +}; |
| 51 | +``` |
| 52 | + |
| 53 | +The promise is typically created by a parent and passed down — don't create it inside the component, or you'll create a new promise on every render. |
| 54 | + |
| 55 | +## `useTransition` |
| 56 | + |
| 57 | +`useTransition` marks a state update as non-urgent so React can keep typing, scrolling, and other urgent input responsive while it renders. |
| 58 | + |
| 59 | +```tsx |
| 60 | +import { useState, useTransition } from "react"; |
| 61 | + |
| 62 | +const TabSwitcher = () => { |
| 63 | + const [isPending, startTransition] = useTransition(); |
| 64 | + const [tab, setTab] = useState<"posts" | "comments">("posts"); |
| 65 | + |
| 66 | + const selectTab = (next: "posts" | "comments") => { |
| 67 | + startTransition(() => { |
| 68 | + setTab(next); |
| 69 | + }); |
| 70 | + }; |
| 71 | + |
| 72 | + return ( |
| 73 | + <> |
| 74 | + <button disabled={isPending} onClick={() => selectTab("posts")}> |
| 75 | + Posts |
| 76 | + </button> |
| 77 | + <button disabled={isPending} onClick={() => selectTab("comments")}> |
| 78 | + Comments |
| 79 | + </button> |
| 80 | + {tab === "posts" ? <Posts /> : <Comments />} |
| 81 | + </> |
| 82 | + ); |
| 83 | +}; |
| 84 | +``` |
| 85 | + |
| 86 | +### Async transitions (React 19) |
| 87 | + |
| 88 | +In React 19, the function passed to `startTransition` can be async. This is the foundation for Actions and is how `useActionState` and `<form action>` schedule their pending state. |
| 89 | + |
| 90 | +```tsx |
| 91 | +const [isPending, startTransition] = useTransition(); |
| 92 | + |
| 93 | +const onSubmit = () => { |
| 94 | + startTransition(async () => { |
| 95 | + await saveDraft(content); |
| 96 | + setSavedAt(new Date()); |
| 97 | + }); |
| 98 | +}; |
| 99 | +``` |
| 100 | + |
| 101 | +`isPending` stays `true` for the entire duration of the async callback, including awaited work. |
| 102 | + |
| 103 | +## `useDeferredValue` |
| 104 | + |
| 105 | +`useDeferredValue` lets you defer re-rendering a part of the UI that's expensive to compute, so urgent updates (typing into an input) can flush first. |
| 106 | + |
| 107 | +```tsx |
| 108 | +import { useDeferredValue, useState } from "react"; |
| 109 | + |
| 110 | +const SearchPage = () => { |
| 111 | + const [query, setQuery] = useState(""); |
| 112 | + const deferredQuery = useDeferredValue(query); |
| 113 | + |
| 114 | + return ( |
| 115 | + <> |
| 116 | + <input value={query} onChange={(e) => setQuery(e.target.value)} /> |
| 117 | + {/* SearchResults re-renders with deferredQuery, lagging behind input */} |
| 118 | + <SearchResults query={deferredQuery} /> |
| 119 | + </> |
| 120 | + ); |
| 121 | +}; |
| 122 | +``` |
| 123 | + |
| 124 | +### `initialValue` (React 19) |
| 125 | + |
| 126 | +React 19 added an optional second argument: the value to use during the initial render before the deferred value has caught up. Useful for SSR/streaming when you want to show a known initial value rather than the latest one. |
| 127 | + |
| 128 | +```tsx |
| 129 | +const deferredQuery = useDeferredValue(query, ""); |
| 130 | +``` |
| 131 | + |
| 132 | +## `startTransition` (standalone) |
| 133 | + |
| 134 | +`startTransition` is also exported directly from `react` for use outside components — for example, inside event handlers in non-React code or third-party stores. |
| 135 | + |
| 136 | +```tsx |
| 137 | +import { startTransition } from "react"; |
| 138 | + |
| 139 | +store.subscribe(() => { |
| 140 | + startTransition(() => { |
| 141 | + forceRender(); |
| 142 | + }); |
| 143 | +}); |
| 144 | +``` |
| 145 | + |
| 146 | +The standalone version does not provide an `isPending` flag — use the hook if you need that. |
| 147 | + |
| 148 | +## See also |
| 149 | + |
| 150 | +- [`useActionState`, `useFormStatus`, `useOptimistic`](https://react.dev/reference/react) — built on top of transitions |
| 151 | +- [Server Components and `'use server'`](https://react.dev/reference/rsc/server-components) |
7 | 152 |
|
8 | 153 | [Something to add? File an issue](https://github.com/typescript-cheatsheets/react/issues/new). |
0 commit comments