You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: use useTransition in the InfiniteList recipe
Yes, it is the right hook, and for a better reason than ergonomics.
The hand-rolled status had a real bug. When `loadPage` resolves from a warm
cache without ever yielding, React batches the update that sets the loading
flag together with the one that clears it. `skip` never changes, so the
observer never re-arms and the list stops after one page. Measured on the same
input: the status version requests [0], this one requests [0, 1, 2, 3, 4].
`isPending` is raised by React when `startTransition` runs and lowered when the
awaited work settles, so it renders either way. It also removes the
`setLoading(false)` that has to be repeated on every exit path, and marking the
append as a transition keeps a list of hundreds of rows from blocking a click.
It also answers the floating-promise objection properly rather than by
deleting `async`: `startTransition` awaits the function it is given, so the
async work now has an owner.
Async transitions need React 19, so the recipe carries a note telling React 18
readers to track the status themselves.
Verified against the browser's own observer, eight cases: first page with no
effect, a short page keeps filling, a warm cache keeps filling, each page
requested exactly once, observation stops on the last page, the live region
announces pending then failure, the button recovers, and a failure does not
become a scroll-retry loop.
@@ -204,11 +206,15 @@ export function InfiniteList<T>({
204
206
205
207
<ObserverDemorecipe="infinite-list" />
206
208
207
-
There is no effect here, and that is the point. An empty list puts the sentinel inside the viewport, so the observer asks for the first page through the same path as every page after it. An effect for the first page would duplicate the fetch, the error handling, and the loading flag, then race the observer for the same request.
209
+
:::note[React 19]
210
+
Passing an async function to `startTransition` needs React 19. On React 18, only the updates you schedule before the first `await` are part of the transition, so `isPending` clears too early. Track a `"idle" | "loading" | "error"` status yourself there.
211
+
:::
212
+
213
+
There is no effect here, and that is the point. An empty list puts the sentinel inside the viewport, so the observer asks for the first page through the same path as every page after it. An effect for the first page would duplicate the fetch, the error handling, and the pending flag, then race the observer for the same request.
208
214
209
-
One `status` beats separate `loading` and `error` booleans: the component cannot be loading and failed at the same time, and the retry button reads its own label from it.
215
+
`useTransition` owns that pending flag. React raises `isPending` when `startTransition` runs and lowers it when the awaited work settles, so there is no `setLoading(true)` to pair with a `setLoading(false)` on every exit path. `startTransition` also awaits the async function you hand it, so nothing floats. Marking the append as a transition is worth it on its own: re-rendering a list that has grown to hundreds of rows no longer blocks a click or a keystroke.
210
216
211
-
`loadMore`is not `async`. Nothing awaits it, so returning a promise would only invite a caller to try. It starts the request and reports the outcome through state, which is what both the observer and the button want.
217
+
A hand-rolled loading boolean is also less reliable here. When `loadPage` resolves from a warm cache without ever yielding, React can batch the update that sets the flag together with the one that clears it. The observer then never sees `skip` change, and the list stops after one page. `isPending` renders either way.
212
218
213
219
`skip` does two jobs. It keeps a second request from starting while one is in flight, and because the hook drops and recreates the observer when `skip` flips back, it re-arms the sentinel. A page too short to push the sentinel out of view keeps loading until the viewport fills.
0 commit comments