reactjsreact-hooks

Why is isPending not turning true immediately when using useTransition with a heavy computation?


I'm testing a heavy calculation that blocks the render of my UI and using a useTransition hook to fix it and show a loading while it is updating.

App.tsx

import { useEffect, useState, useTransition } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    console.log("Starting Transition...");
    startTransition(() => {
      for (let i = 0; i < 1000000000; i++) {}
      setCount((prevCount) => prevCount + 1);
      console.log("Ending Transition...");
    });
  };

  useEffect(() => {
    console.log(isPending);
  }, [isPending]);

  return (
    <div className="App">
      <div>
        <h1>{count}</h1>
        <button disabled={isPending} onClick={handleClick}>
          {isPending ? "Loading..." : "Increment"}
        </button>
      </div>
      {isPending && "Loading"}
    </div>
  );
}

The problem is the isPending is turning to true only a short time after the useTransition is done, and the logs are like this when I click the button:

Logs

Starting Transition...
Ending Transition...
true
false

My question is why isPending is not turning true when the transition starts and not after it ends?


Solution

  • startTransition does not cause a synchronous rerender when you call it. Synchronous rerenders are a thing that exists, but they can be a performance problem, and transitions are trying to improve perceived performance, not hurt it.

    Instead, when you call startTransition you tell react "any calls i make to set state inside this function are low priority". React takes note of this, and then calls your function, and then you set state as many times as you like. Once your function returns, react queues up a normal priority rerender to set isPending to true. After that, it does a low priority render to apply the state changes you made.

    So startTransition is not designed for you to do some large amount of synchronous work. It will not help at all with that. You might call startTransition when you're done with some heavy work. But that's useful not because it breaks up your synchronous code, but rather because the resulting render will be low priority. And a low priority render is useful if the render itself might take a while, or if you expect the user will be doing high priority things (eg, typing into an input field) which you don't want to slow down.

    If you want to split up some synchronous code of yours, you will have to write custom code to do that. For example, you can have a series of setTimeouts each of which run a bit of code, then queue up the next setTimeout, thus giving the browser a chance to run other code.