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 tryuseDeferredValue
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 needuseTransition
instead.- Other than that, all other caveats of
useTransition
are applicable tostartTransition
.
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 touseTransition
, except that it does not provide theisPending
flag to track whether a Transition is ongoing. We can callstartTransition
whenuseTransition
is not available. For example,startTransition
works outside components, such as from a data library.