React.js
Advanced Hooks
UseTransition

UseTransition

Its a built in hook (React 18 and newer) that allows you to diferentiate between urgent and non urgent (and non blocking at the same time) updates.

Implementation

This hook returns a tuple, where the first element is pending variable, and the second is a startTransition function.

const [pending, startTransition] = useTransition();

The pending variable is a boolean that indicates if there is a pending transition. The startTransition function is used to wrap a piece of code, that will be executed in a non blocking way.

startTransition(() => {
  // code that will be executed in a non blocking way
});

Example

useTransition.tsx
const Posts = memo(function PostsTab() {
  let items = [];
  for (let i = 0; i < 2000; i++) {
    items.push(<SlowPost key={i} index={i} />);
  }
  return (
    <div>
      <p>Posts (hidden for demo purposes)</p>
      <ul style={{ display: "none" }}>{items}</ul>
    </div>
  );
});
 
function SlowPost({ index }) {
  let startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // Do nothing for 1 millisecond
  }
  return <li className="item">Post #{index + 1}</li>;
}
 
export const WithTransition = () => {
  const [tabToShow, setTabToShow] = useState("one");
  const [isPending, startTransition] = useTransition();
 
  const handleClick = (tab) => {
    console.log(`Switching to tab ${tab}`);
    startTransition(() => {
      console.log(`Transitioning to tab ${tab}`);
      setTabToShow(tab);
      console.log(`Transitioned to tab ${tab}`);
    });
    console.log(`Switched to tab ${tab}`);
    console.log("-----");
  };
 
  return (
    <div>
      <Button onClick={() => handleClick("one")} disabled={tabToShow === "one"}>
        Show tab one
      </Button>
      <Button onClick={() => handleClick("two")} disabled={tabToShow === "two"}>
        Show expensive tab
      </Button>
      <Button
        onClick={() => handleClick("three")}
        disabled={tabToShow === "three"}
      >
        Show tab three
      </Button>
      <div>
        {tabToShow === "one" && <p>Tab one</p>}
        {tabToShow === "two" && <Posts />}
        {tabToShow === "three" && <p>Tab three</p>}
      </div>
    </div>
  );
};

Demo

Tab one

Console:
  • -no logs-

In example above, you can see that SlowPost will artificially slow down the rendering of the app. If you click on the Show expensive tab button, you will see that the app will not freeze, and you will be able to interact with it. This is because the startTransition is wrapping setTabToShow function, and it will be executed as not priority task. Even if the state of tabToShow is not updated and you still cant see the posts, or the button to be disabled, you can still change tab to another one, because the that state update will be deffered.

Another words, react knows that setTabToShow value will change what is rendered on the screen, and it will deffer that change if component that needs to be rendered is not ready yet, due to some expensive computation. At the same time, because we wrapped that inside startTransition, we can still interact with the app, to change that state to something else, that will be rendered immediately.

💡

Because of how i implemented this fake console in the demo above, you can actualy observe what react is doing under the hood: Once you click on the Show expensive tab you will see, that it will log Switching to tab two and Switched to tab two immediately. Then, when our slow component is ready, it will not log Transitioning to tab two and Transitioned to tab two as you might expect- instead it replaces whole state of the application, to represent synchronous state change! If you check the actual browser console, you will see the same order of logs, but you will not observe that state change.

Pending

Lets try to make use of the pending variable, with some really simple example:

// ...
return (
  <div>
    <Button
      onClick={() => handleClick("one")}
      disabled={tabToShow === "one" && !isPending}
    >
      Show tab one
    </Button>
    <Button
      onClick={() => handleClick("two")}
      disabled={tabToShow === "two" || isPending}
    >
      Show expensive tab
    </Button>
    <Button
      onClick={() => handleClick("three")}
      disabled={tabToShow === "three" && !isPending}
    >
      Show tab three
    </Button>
    {isPending && <p>Loading...</p>}
    {!isPending && (
      <>
        {tabToShow === "one" && <p>Tab one</p>}
        {tabToShow === "two" && <Posts />}
        {tabToShow === "three" && <p>Tab three</p>}
      </>
    )}
  </div>
);

Demo

Tab one

Console:
  • -no logs-

Now interface seems to be very responsive. You can even observe, how pending state will change even with components that renders really fast.

Without useTransition

In example below, you can check how same component behaves without useTransition hook.

//...
const handleClick = (tab) => {
  log(`Switching to tab ${tab}`);
 
  log(`Before setTabToShow(${tab})`);
  setTabToShow(tab);
  log(`After setTabToShow(${tab})`);
 
  log(`Switched to tab ${tab}`);
  log("-----");
};
//...

Demo

Tab one

Console:
  • -no logs-

vs setTimeout

One more thing to note, is how different setTimeout behaves, compared to useTransition. Set setTransition is not asynchronous task, so our console.logs appear in order as soon as task is executed. setTimeout on the other hand, is asynchronous task, so our console.logs inside will appear after the whole handleClick function is done, once synchronous queue is empty.

//...
const handleClick = (tab) => {
  log(`Switching to tab ${tab}`);
 
  setTimeout(() => {
    log(`Timeout begins: ${tab}`);
    setTabToShow(tab);
    log(`Timeout ends: ${tab}`);
    log("-----");
  }, 0);
 
  log(`Switched to tab ${tab}`);
};
//...

Demo

Tab one

Console:
  • -no logs-
💡

Again, as you can see, setTimeout logs appears in different order: Switching, Switched, Timeout begins, Timeout ends. If task inside setTimeout takes some time, last two logs will appear after that task is done, so after some delay. On the other hand, during startTransition logs will appear in order: Switching, Transitioning, Transitioned, Switched, as soon as render is ready, so in the end, order of execution is synchronous.