useTransition — React Hook

AkashSDas
5 min readMar 29, 2024

--

The useTransition hook lets us update the state without blocking the UI.

It provides a way to declaratively coordinate animations or transitions with React's rendering cycle, enabling smoother user experiences. Following is it the signature of useTransition hook:

const [isPending, startTransition] = useTransition();

It takes no argument and return two values. One being isPending flag that tells us whether there is a pending Transition., and other being the startTransition function that lets us mark a state update as a Transition.

The function passed inside the startTransition, that updates some state by calling one or more set functions. React immediately calls scope with no parameters and marks all state updates scheduled synchronously during the scope function call as Transitions. They will be non-blocking and will not display unwanted loading indicators.

import { memo, useState, useTransition } from "react";

export default function App(): JSX.Element {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState("about");

function selectTab(nextTab: string) {
startTransition(() => {
setTab(nextTab);
});
}

return (
<div>
<TabButton
label="About"
tabId="about"
selectTab={selectTab}
currentTab={tab}
/>
<TabButton
label="Posts (slow)"
tabId="posts"
selectTab={selectTab}
currentTab={tab}
/>
<TabButton
label="Contact"
tabId="contact"
selectTab={selectTab}
currentTab={tab}
/>

{isPending && <p>Loading...</p>}
<hr />
<TabContent tab={tab} />
</div>
);
}

function TabButton({
label,
tabId,
selectTab,
currentTab,
}: {
label: string;
tabId: string;
selectTab: (tabId: string) => void;
currentTab: string;
}): JSX.Element {
const isActive = currentTab === tabId;

return (
<button
onClick={() => selectTab(tabId)}
style={{ fontWeight: isActive ? "bold" : "normal" }}
>
{label}
</button>
);
}

function TabContent({ tab }: { tab: string }): JSX.Element | null {
switch (tab) {
case "about":
return <AboutTab />;
case "posts":
return <PostsTab />;
case "contact":
return <ContactTab />;
default:
return null;
}
}

function AboutTab(): JSX.Element {
return <p>About page</p>;
}

function ContactTab(): JSX.Element {
return <p>Contact page</p>;
}

const PostsTab = memo(function PostsTab() {
const items: JSX.Element[] = [];
for (let i = 0; i < 500; i++) {
items.push(<SlowPost key={i} index={i} />);
}
return <ul className="items">{items}</ul>;
});

function SlowPost({ index }: { index: number }): JSX.Element {
const startTime = performance.now();
while (performance.now() - startTime < 1) {
// Do nothing for 1 ms per item to emulate extremely slow code
}

return <li className="item">Post #{index + 1}</li>;
}

Key details of useTransition hook:

  • Transition updates can’t be used to control text inputs.
  • This hook can only be called inside components or custom hooks. If we need to start a Transition somewhere else (for example, from a data library), then we’ve to call the standalone startTransition instead.
  • We can wrap an update into a Transition only if we’ve access to the set function of that state. If we want to start a Transition in response to some prop or a custom hook value, then try useDeferredValue instead.
  • The function we pass to startTransition must be synchronous. React immediately executes this function, marking all state updates that happen while it executes as Transitions. If we try to perform more state updates later (for example, in a timeout), they won’t be marked as Transitions.
  • A state update marked as a Transition will be interrupted by other state updates. For example, if we update a chart component inside a Transition, but then start typing into an input while the chart is in the middle of a re-render, React will restart the rendering work on the chart component after handling the input update.
import { ChangeEvent, useState, useTransition } from "react";

export default function App(): JSX.Element {
const [input, setInput] = useState<string>("");
const [lists, setLists] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();

function handleChange(e: ChangeEvent<HTMLInputElement>) {
const { value } = e.target;
setInput(value);

startTransition(() => {
const dataList: string[] = [];
for (let i: number = 0; i < 100_000; i++) {
dataList.push(value);
}
setLists(dataList);
});
}

return (
<div>
<input type="text" value={input} onChange={handleChange} />

{isPending ? (
<div>Loading...</div>
) : (
lists.map((list: string) => {
return <div key={list}>{list}</div>;
})
)}
</div>
);
}

If there’re multiple ongoing Transitions, React currently batches them together. This is a limitation that will likely be removed in a future release.

We can also update the parent component’s state in a Transition.

Two benefits of using Transition:

  • Transitions are interruptible, which lets the user click away without waiting for the re-render to complete.
  • Transitions prevent unwanted loading indicators, which lets the user avoid jarring jumps on navigation.

We can also use Transition with Suspense.

We can also display errors using Transition, but this feature is currently only available in React canary.

startTransition

startTransition lets us update the state without blocking the UI. Following is its signuature:

startTransition(scope);

Key details:

  • startTransition does not provide a way to track whether a Transition is pending. To show a pending indicator while the Transition is ongoing, we need useTransition instead.
  • Other than that, all other caveats of useTransition are applicable to startTransition.
import { memo, startTransition, useState } from "react";

export default function App(): JSX.Element {
const [tab, setTab] = useState("about");

function selectTab(nextTab: string) {
startTransition(function () {
setTab(nextTab);
});
}

return (
<div>
<TabButton
label="About"
tabId="about"
selectTab={selectTab}
currentTab={tab}
/>
<TabButton
label="Posts (slow)"
tabId="posts"
selectTab={selectTab}
currentTab={tab}
/>
<TabButton
label="Contact"
tabId="contact"
selectTab={selectTab}
currentTab={tab}
/>

<hr />
<TabContent tab={tab} />
</div>
);
}

function TabButton({
label,
tabId,
selectTab,
currentTab,
}: {
label: string;
tabId: string;
selectTab: (tabId: string) => void;
currentTab: string;
}): JSX.Element {
const isActive = currentTab === tabId;

return (
<button
onClick={() => selectTab(tabId)}
style={{ fontWeight: isActive ? "bold" : "normal" }}
>
{label}
</button>
);
}

function TabContent({ tab }: { tab: string }): JSX.Element | null {
switch (tab) {
case "about":
return <AboutTab />;
case "posts":
return <PostsTab />;
case "contact":
return <ContactTab />;
default:
return null;
}
}

function AboutTab(): JSX.Element {
return <p>About page</p>;
}

function ContactTab(): JSX.Element {
return <p>Contact page</p>;
}

const PostsTab = memo(function PostsTab() {
const items: JSX.Element[] = [];
for (let i = 0; i < 500; i++) {
items.push(<SlowPost key={i} index={i} />);
}
return <ul className="items">{items}</ul>;
});

function SlowPost({ index }: { index: number }): JSX.Element {
const startTime = performance.now();
while (performance.now() - startTime < 1) {
// Do nothing for 1 ms per item to emulate extremely slow code
}

return <li className="item">Post #{index + 1}</li>;
}

startTransition is very similar to useTransition, except that it does not provide the isPending flag to track whether a Transition is ongoing. We can call startTransition when useTransition is not available. For example, startTransition works outside components, such as from a data library.

--

--