r/reactjs 1d ago

Needs Help What does "rendering in background" in startTransition really mean?

So far, I understand that wrapping a function with startTransition tells React to treat it as a non‑urgent update. So if any urgent action occurs, React can respond to it immediately without blocking.

But here is where I got stuck. The docs say:

“useTransition is a React Hook that lets you render a part of the UI in the background.”

“The function passed to startTransition is called the Action. You can update state and (optionally) perform side effects within an Action, and the work will be done in the background without blocking user interactions.”

I don’t really get what “in the background” really means.

Looking at the example, I don’t understand why, with startTransition, the “Total” only renders once with the final "Total" after clicking “quantity” multiple times, instead of updating multiple times according to the number of times the “quantity” was clicked

Does “run in background” prevent multiple renders and only show the final result??

20 Upvotes

15 comments sorted by

5

u/Western_Insurance764 1d ago

It's not that it prevents multiple renders exactly, it’s about priority. When you put the state update inside startTransition, React marks it as low priority so it can be interrupted. If you click the button 3 times fast, React starts working on the first update, then sees the second click come in as more urgent and just drops the first render to start the next one. So you only see the last result because the intermediate states got abandoned before they could finish

The "background" wording is confusing. It basically means React does the work when it has free time, like in idle moment, not blocking the main thread for important stuff like typing in a input

3

u/Neat_Living_6765 1d ago

Hi, thanks for your comment!

I also thought that the early update schedules (early actions) got abandoned until I read this term:

https://react.dev/reference/react/useTransition#my-state-updates-in-transitions-are-out-of-order

"When clicking multiple times, it’s possible for previous requests to finish after later requests. When this happens, React currently has no way to know the intended order. This is because the updates are scheduled asynchronously, and React loses context of the order across the async boundary."

The first sentence implies that the “early actions” have not been abandoned.

(I’m not an English native speaker, but I hope you get what I mean...)

2

u/Tomus 17h ago

If you look at the example, this is about some async IO causing race conditions. The point of that doc is that you need to wrap the state setter in startTransition again after the await even if you're already inside a startTransition. It's not really about what React is doing under the hood, just a technical limitation on what React can track right now. If you want to look deeper, AsyncContext will allow React to track this without the nested startTransition call, it's a web API proposal right now.

2

u/kurtextrem 1d ago

It means react will split up the rendering work. If the React render takes a while (because a lot of components need to re-render for example), it will yield to the main thread every 5ms. This allows for example paints to happen and keeps INP low on interactions: https://kurtextrem.de/posts/improve-inp-react#enabling-concurrent-rendering-.

Also, it batches. As written in the article, if the renders take a while, only the last one will actually commit (aka. update the DOM). So if you have 3 updates, only the last one will modify the DOM. And if you use "useTransition" React aborts prior renders, otherwise it'll wait them out but still only flush the most recent state update to the DOM.

1

u/Neat_Living_6765 1d ago

I also thought it might batch update, but I'm skeptical because there are three set state from three different event handler triggers. It is not in one event handler trigger.

As for aborting, I explained in the comment above why I’m not sure whether the previous renders were actually aborted.

1

u/Tomus 17h ago

Transitions across multiple event handlers will batch assuming they all finish within the same 300ms window. You can easily create a demo to see this happening.

This is in contrast to high priority state updates (i.e. regular state updates not in a transition) where they're only batched within a single event handler. These state updates flush the render before another event handler can fire.

1

u/Neat_Living_6765 15h ago

Could you add two console.log as below and run the code without StrictMode.

Increment quantity to 3 by clicking twice quickly. Below is the log I observed:

quantity: 1   // initial render
(2) quantity: 1   // rendered twice because setClientQuantity(newQuantity) was called twice due to double clicks
async done: 3   // setQuantity(3) is scheduled
quantity: 1 => ???   // I don’t understand why it logs 1 here
async done: 2   // setQuantity(2) is scheduled, so the render with quantity 3 is abandoned
quantity: 1 => ???   // again, I don’t understand why it logs 1 here
quantity: 2
===============
What I expected:
quantity:  1 
(2) quantity:  1 
async done:  3 
async done:  2
quantity:  2



export default function App() {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  const [clientQuantity, setClientQuantity] = useState(1);

  console.log("quantity: ", quantity);

  const updateQuantityAction = (newQuantity) => {
    setClientQuantity(newQuantity);

    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        console.log("async done: ", savedQuantity);
        setQuantity(savedQuantity);
      });
    });
  };

2

u/Tomus 10h ago

Don't trust console.logs absolutely, just because something ran doesn't mean React actually flushed the update and rendered to the screen.

The easiest way to inspect what's going on is looking at Chrome devtools filmstrip view (it will show you very fast paints that you may miss with the your eyes) and React devtools timeline view.

Feel free to share a full code snippet (eg. stackblitz link) and I'll try to explain what's going on.

1

u/Neat_Living_6765 9h ago

Yes, actually, I used the Profiler and recorded the action when I increased the quantity to 3. Apart from the initial render, it showed two renders, as expected.

It’s just that whenever I notice something unusual in the console log, or even come across a term in the docs like “background,” my mind can’t stop thinking about it 😅😅

I’d appreciate it if you could explain why I shouldn’t completely trust the console log. I’ve never heard anyone say that before

1

u/CoffeeToCodee 1d ago

In the background” doesn’t mean React is rendering on another thread. It means the transition update is treated as lower priority. React can start rendering it, pause or discard that work if a more urgent update happens, and then continue/restart it afterward. That’s why the UI can stay responsive during a transition.

1

u/Vincent_CWS 21h ago

conceptually there is an outer Scheduler loop and an inner React Fiber loop. They are not both running

continuously; the outer loop invokes the inner loop for one time slice.

Scheduler.workLoop() // outer loop: tasks

└─ performConcurrentWorkOnRoot()

└─ renderRootConcurrent()

└─ workLoopConcurrent() // inner loop: Fibers

├─ performUnitOfWork()

├─ performUnitOfWork()

└─ shouldYield() === true

so the background means they are in these two loops

1

u/Still-Constant3085 13h ago

I think ‘background’ is slightly misleading here because it makes you picture another thread. I find it easier to think of startTransition as React saying: ‘this update can wait.’ React can begin rendering it, yield when something more important happens, and restart or discard unfinished work. The user only sees a render once React commits it.

1

u/bullishshorts 5h ago

React compares the old virtual tree to new virtual tree and build a tree that basically holds the minimal changes react has to apply to the dom. Remove this node, update that, add this … etc

This comparison / reconciliation algo is expansive. It is what takes time and blocks the UI if your old / new dom is huge.

It used to be one shot synchronous call before React fiber. The browser of course cant take any other tasks if it is running it. Hence if you are adding like thousands of HTMl nodes, the UI freezes. Not because the browser is painting them, but because react is reconciling them.

React fiber sort of split the reconciliation task into multiple chunks. React runs a small portion of it (aka push is a small job to the call stack) and stops, so the browser can pick up any other tasks in the call stack (say a button click callback). Then react pushes another portion of it where it detects the browsers isn’t busy with other calls … etc. It is a bit more complicated than that but that’s the gist of it.

Once that update tree is ready, the browser paints (aka actually applies the changes to the dom) and that is fast and not usually the culprit.

Keep in mind this is not to be confused with async.