useTransition

useTransition은 UI를 차단하지 않고 상태를 업데이트할 수 있는 React Hook입니다.

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

const [isPending, startTransition] = useTransition()

레퍼런스

useTransition()

컴포넌트의 최상위 수준에서 useTransition을 호출하여 일부 state 업데이트를 Transition 으로 표시합니다.

import { useTransition } from 'react';

function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}

아래에서 더 많은 예시를 확인하세요.

매개변수

useTransition은 어떤 매개변수도 받지 않습니다.

반환값

useTransition은 정확히 두 개의 항목이 있는 배열을 반환합니다.

  1. isPending 플래그는 대기 중인 Transition 이 있는지 알려줍니다.
  2. startTransition 함수는 상태 업데이트를 Transition 으로 표시할 수 있게 해주는 함수입니다.

useTransition이 반환하는 startTransition 함수를 사용하면 state 업데이트를 Transition 으로 표시할 수 있습니다.

  1. The isPending flag that tells you whether there is a pending Transition.
  2. The startTransition function that lets you mark updates as a Transition.

startTransition(action)

The startTransition function returned by useTransition lets you mark an update as a Transition.

function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}

중요합니다!

Functions called in startTransition are called “Actions”.

The function passed to startTransition is called an “Action”. By convention, any callback called inside startTransition (such as a callback prop) should be named action or include the “Action” suffix:

function SubmitButton({ submitAction }) {
const [isPending, startTransition] = useTransition();

return (
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
submitAction();
});
}}
>
Submit
</button>
);
}

Parameters

  • action: A function that updates some state by calling one or more set functions. React calls action immediately with no parameters and marks all state updates scheduled synchronously during the action function call as Transitions. Any async calls that are awaited in the action will be included in the Transition, but currently require wrapping any set functions after the await in an additional startTransition (see Troubleshooting). State updates marked as Transitions will be non-blocking and will not display unwanted loading indicators.

b1a249d597016c6584e4c186daa28b180cc9aafc

반환값

startTransition은 아무것도 반환하지 않습니다.

주의 사항

  • useTransition은 Hook이므로 컴포넌트나 커스텀 Hook 내부에서만 호출할 수 있습니다. 다른 곳(예시: 데이터 라이브러리)에서 Transition 을 시작해야 하는 경우, 독립형 startTransition을 호출하세요.

  • 해당 state의 set 함수에 액세스할 수 있는 경우에만 업데이트를 Transition 으로 래핑할 수 있습니다. 일부 prop이나 커스텀 Hook 값에 대한 응답으로 Transition 을 시작하려면 useDeferredValue를 사용해 보세요.

  • startTransition에 전달하는 함수는 동기식이어야 합니다. React는 이 함수를 즉시 실행하여 실행하는 동안 발생하는 모든 state 업데이트를 Transition 으로 표시합니다. 나중에 더 많은 state 업데이트를 수행하려고 하면(예시: timeout), Transition 으로 표시되지 않습니다.

  • The function you pass to startTransition is called immediately, marking all state updates that happen while it executes as Transitions. If you try to perform state updates in a setTimeout, for example, they won’t be marked as Transitions.

  • You must wrap any state updates after any async requests in another startTransition to mark them as Transitions. This is a known limitation that we will fix in the future (see Troubleshooting).

  • The startTransition function has a stable identity, so you will often see it omitted from Effect dependencies, but including it will not cause the Effect to fire. If the linter lets you omit a dependency without errors, it is safe to do. Learn more about removing Effect dependencies.

  • Transition으로 표시된 state 업데이트는 다른 state 업데이트에 의해 중단됩니다. 예를 들어, Transition 내에서 차트 컴포넌트를 업데이트한 다음 차트가 다시 렌더링 되는 도중에 입력을 시작하면 React는 입력 업데이트를 처리한 후 차트 컴포넌트에서 렌더링 작업을 다시 시작합니다.

  • Transition 업데이트는 텍스트 입력을 제어하는 데 사용할 수 없습니다.

  • 진행 중인 Transition 이 여러 개 있는 경우, React는 현재 Transition 을 함께 일괄 처리합니다. 이는 향후 릴리즈에서 제거될 가능성이 높은 제한 사항입니다.

사용법

Perform non-blocking updates with Actions

Call useTransition at the top of your component to create Actions, and access the pending state:

import {useState, useTransition} from 'react';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
// ...
}

useTransition은 정확히 두 개의 항목이 있는 배열을 반환합니다.

  1. 보류 중인 Transition 이 있는지를 알려주는 isPending 플래그입니다.
  2. state 업데이트를 Transition 으로 표시할 수 있는 startTransition 함수입니다.

그 후 다음과 같이 state 업데이트를 Transition 으로 표시할 수 있습니다.

  1. The isPending flag that tells you whether there is a pending Transition.
  2. The startTransition function that lets you create an Action.

To start a Transition, pass a function to startTransition like this:

import {useState, useTransition} from 'react';
import {updateQuantity} from './api';

function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);

function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ...
}

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 on the page. A Transition can include multiple Actions, and while a Transition is in progress, your UI stays responsive. For example, if the user clicks a tab but then changes their mind and clicks another tab, the second click will be immediately handled without waiting for the first update to finish.

To give the user feedback about in-progress Transitions, to isPending state switches to true at the first call to startTransition, and stays true until all Actions complete and the final state is shown to the user. Transitions ensure side effects in Actions to complete in order to prevent unwanted loading indicators, and you can provide immediate feedback while the Transition is in progress with useOptimistic.

The difference between Actions and regular event handling

예시 1 of 2:
Updating the quantity in an Action

In this example, the updateQuantity function simulates a request to the server to update the item’s quantity in the cart. This function is artificially slowed down so that it takes at least a second to complete the request.

Update the quantity multiple times quickly. Notice that the pending “Total” state is shown while any requests are in progress, and the “Total” updates only after the final request is complete. Because the update is in an Action, the “quantity” can continue to be updated while the request is in progress.

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

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

  const updateQuantityAction = async newQuantity => {
    // To access the pending state of a transition,
    // call startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total quantity={quantity} isPending={isPending} />
    </div>
  );
}

This is a basic example to demonstrate how Actions work, but this example does not handle requests completing out of order. When updating the quantity multiple times, it’s possible for the previous requests to finish after later requests causing the quantity to update out of order. This is a known limitation that we will fix in the future (see Troubleshooting below).

For common use cases, React provides built-in abstractions such as:

These solutions handle request ordering for you. When using Transitions to build your own custom hooks or libraries that manage async state transitions, you have greater control over the request ordering, but you must handle it yourself.


Exposing action prop from components

You can expose an action prop from a component to allow a parent to call an Action.

For example, this TabButton component wraps its onClick logic in an action prop:

export default function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
action();
});
}}>
{children}
</button>
);
}

부모 컴포넌트가 onClick 이벤트 핸들러 내에서 state를 업데이트하기 때문에 해당 state 업데이트는 Transition 으로 표시됩니다. 그렇기 때문에 앞의 예시에서처럼 “posts”을 클릭한 다음 바로 “Contact”를 클릭할 수 있습니다. 선택한 탭을 업데이트하는 것은 Transition 으로 표시되므로 사용자 상호작용을 차단하지 않습니다.

Because the parent component updates its state inside the action, that state update gets marked as a Transition. This means you can click on “Posts” and then immediately click “Contact” and it does not block user interactions:

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


Displaying a pending visual state

useTransition이 반환하는 isPending boolean 값을 사용하여 transition이 진행 중임을 사용자에게 표시할 수 있습니다. 예를 들어 탭 버튼은 특별한 “pending” 시각적 상태를 가질 수 있습니다.

function TabButton({ action, children, isActive }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...

이제 탭 버튼 자체가 바로 업데이트되므로 “Posts”을 클릭하는 반응이 더 빨라진 것을 확인할 수 있습니다.

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}


원치 않는 로딩 표시기 방지

이 예시에서 PostsTab 컴포넌트는 Suspense-enabled 데이터 소스를 사용하여 일부 데이터를 가져옵니다. “Posts” 탭을 클릭하면 PostsTab 컴포넌트가 suspends 되어 가장 가까운 로딩 폴백이 나타납니다.

In this example, the PostsTab component fetches some data using use. When you click the “Posts” tab, the PostsTab component suspends, causing the closest loading fallback to appear:

import { Suspense, useState } from 'react';
import TabButton from './TabButton.js';
import AboutTab from './AboutTab.js';
import PostsTab from './PostsTab.js';
import ContactTab from './ContactTab.js';

export default function TabContainer() {
  const [tab, setTab] = useState('about');
  return (
    <Suspense fallback={<h1>🌀 Loading...</h1>}>
      <TabButton
        isActive={tab === 'about'}
        action={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        action={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        action={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}

로딩 표시기를 표시하기 위해 전체 탭 컨테이너를 숨기면 사용자 경험이 어색해집니다. TabButtonuseTransition을 추가하면 탭 버튼에 보류 중인 상태를 표시할 수 있습니다.

Hiding the entire tab container to show a loading indicator leads to a jarring user experience. If you add useTransition to TabButton, you can instead display the pending state in the tab button instead.

“Posts”을 클릭하면 더 이상 전체 탭 컨테이너가 스피너로 바뀌지 않습니다.

import { useTransition } from 'react';

export default function TabButton({ action, children, isActive }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  if (isPending) {
    return <b className="pending">{children}</b>;
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        action();
      });
    }}>
      {children}
    </button>
  );
}

Suspense에서 Transition 을 사용하는 방법에 대해 자세히 알아보세요.

중요합니다!

Transition은 이미 표시된 콘텐츠(예시: 탭 컨테이너)를 숨기지 않을 만큼만 “대기”합니다. 만약 Posts 탭에 중첩된 <Suspense> 경계가 있는 경우 Transition 은 이를 “대기”하지 않습니다.

Transitions only “wait” long enough to avoid hiding already revealed content (like the tab container). If the Posts tab had a nested <Suspense> boundary, the Transition would not “wait” for it.


Suspense-enabled 라우터 구축

React 프레임워크나 라우터를 구축하는 경우 페이지 탐색을 Transition 으로 표시하는 것이 좋습니다.

function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();

function navigate(url) {
startTransition(() => {
setPage(url);
});
}
// ...

두 가지 이유로 이 방법을 권장합니다.

다음은 탐색을 위해 Transition 을 사용하는 아주 간단한 라우터 예시입니다.

This is recommended for three reasons:

Here is a simplified router example using Transitions for navigations.

import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';

export default function App() {
  return (
    <Suspense fallback={<BigSpinner />}>
      <Router />
    </Suspense>
  );
}

function Router() {
  const [page, setPage] = useState('/');
  const [isPending, startTransition] = useTransition();

  function navigate(url) {
    startTransition(() => {
      setPage(url);
    });
  }

  let content;
  if (page === '/') {
    content = (
      <IndexPage navigate={navigate} />
    );
  } else if (page === '/the-beatles') {
    content = (
      <ArtistPage
        artist={{
          id: 'the-beatles',
          name: 'The Beatles',
        }}
      />
    );
  }
  return (
    <Layout isPending={isPending}>
      {content}
    </Layout>
  );
}

function BigSpinner() {
  return <h2>🌀 Loading...</h2>;
}

중요합니다!

Suspense-enabled 라우터는 기본적으로 탐색 업데이트를 Transition 으로 래핑할 것으로 예상됩니다.


Error boundary로 사용자에게 오류 표시하기

startTransition에 전달된 함수가 오류를 발생시키면 error boundary를 사용하여 사용자에게 오류를 표시할 수 있습니다. error boundary를 사용하려면 useTransition을 호출하는 컴포넌트를 error boundary로 래핑하세요. startTransition에 전달된 함수에서 오류가 발생하면, error boundary의 fallback이 표시됩니다.

If a function passed to startTransition throws an error, you can display an error to your user with an error boundary. To use an error boundary, wrap the component where you are calling the useTransition in an error boundary. Once the function passed to startTransition errors, the fallback for the error boundary will be displayed.

import { useTransition } from "react";
import { ErrorBoundary } from "react-error-boundary";

export function AddCommentContainer() {
  return (
    <ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
      <AddCommentButton />
    </ErrorBoundary>
  );
}

function addComment(comment) {
  // For demonstration purposes to show Error Boundary
  if (comment == null) {
    throw new Error("Example Error: An error thrown to trigger error boundary");
  }
}

function AddCommentButton() {
  const [pending, startTransition] = useTransition();

  return (
    <button
      disabled={pending}
      onClick={() => {
        startTransition(() => {
          // Intentionally not passing a comment
          // so error gets thrown
          addComment();
        });
      }}
    >
      Add comment
    </button>
  );
}


Troubleshooting

Transition에서 입력 업데이트가 작동하지 않습니다

입력을 제어하는 state 변수에는 Transition 을 사용할 수 없습니다.

const [text, setText] = useState('');
// ...
function handleChange(e) {
// ❌ 제어된 입력 state에 Transition 을 사용할 수 없습니다.
startTransition(() => {
setText(e.target.value);
});
}
// ...
return <input value={text} onChange={handleChange} />;

이는 Transition 이 non-blocking이지만, 변경 이벤트에 대한 응답으로 입력을 업데이트하는 것은 동기적으로 이루어져야 하기 때문입니다. 입력에 대한 응답으로 Transition 을 실행하려면 두 가지 옵션이 있습니다.

  1. 두 개의 개별 state 변수를 선언할 수 있습니다. 하나는 입력 state(항상 동기적으로 업데이트됨) 용이고 다른 하나는 Transition 시 업데이트할 state입니다. 이를 통해 동기 state를 사용하여 입력을 제어하고 (입력보다 “지연”되는) Transition state 변수를 나머지 렌더링 로직에 전달할 수 있습니다.
  2. 또는 state 변수가 하나 있고 실제 값보다 “지연”되는 useDeferredValue를 추가할 수 있습니다. 그러면 non-blocking 리렌더링이 새로운 값을 자동으로 “따라잡기” 위해 트리거됩니다.

React가 state 업데이트를 transition으로 처리하지 않습니다

state 업데이트를 transition으로 래핑할 때는 startTransition 호출 도중에 발생해야 합니다.

startTransition(() => {
// ✅ startTransition 호출 *도중* state 설정
setPage('/about');
});

startTransition에 전달하는 함수는 동기식이어야 합니다. You can’t mark an update as a Transition like this:

startTransition(() => {
// ❌ startTransition 호출 *후에* state 설정
setTimeout(() => {
setPage('/about');
}, 1000);
});

대신 다음과 같이 할 수 있습니다.

setTimeout(() => {
startTransition(() => {
// ✅ startTransition 호출 *도중* state 설정
setPage('/about');
});
}, 1000);

React doesn’t treat my state update after await as a Transition

When you use await inside a startTransition function, the state updates that happen after the await are not marked as Transitions. You must wrap state updates after each await in a startTransition call:

startTransition(async () => {
await someAsyncFunction();
// ❌ Not using startTransition after await
setPage('/about');
});

하지만 이 방법이 대신 동작합니다.

startTransition(async () => {
await someAsyncFunction();
// ✅ Using startTransition *after* await
startTransition(() => {
setPage('/about');
});
});

This is a JavaScript limitation due to React losing the scope of the async context. In the future, when AsyncContext is available, this limitation will be removed.


컴포넌트 외부에서 useTransition을 호출하고 싶습니다

Hook이기 때문에 컴포넌트 외부에서 useTransition을 호출할 수 없습니다. 이 경우 대신 독립형 startTransition 메서드를 사용하세요. 동일한 방식으로 작동하지만 isPending 표시기를 제공하지 않습니다.


startTransition에 전달한 함수는 즉시 실행됩니다

이 코드를 실행하면 1, 2, 3이 출력됩니다.

console.log(1);
startTransition(() => {
console.log(2);
setPage('/about');
});
console.log(3);

1, 2, 3을 출력할 것으로 예상됩니다. startTransition에 전달한 함수는 지연되지 않습니다. 브라우저 setTimeout과 달리 나중에 콜백을 실행하지 않습니다. React는 함수를 즉시 실행하지만, 함수가 실행되는 동안 예약된 모든 상태 업데이트는 Transition 으로 표시됩니다. 아래와 같이 작동한다고 상상하면 됩니다.

// React 작동 방식의 간소화된 버전

let isInsideTransition = false;

function startTransition(scope) {
isInsideTransition = true;
scope();
isInsideTransition = false;
}

function setState() {
if (isInsideTransition) {
// ... Transition state 업데이트 예약 ...
} else {
// ... 긴급 state 업데이트 예약 ...
}
}

My state updates in Transitions are out of order

If you await inside startTransition, you might see the updates happen out of order.

In this example, the updateQuantity function simulates a request to the server to update the item’s quantity in the cart. This function artificially returns the every other request after the previous to simulate race conditions for network requests.

Try updating the quantity once, then update it quickly multiple times. You might see the incorrect total:

import { useState, useTransition } from "react";
import { updateQuantity } from "./api";
import Item from "./Item";
import Total from "./Total";

export default function App({}) {
  const [quantity, setQuantity] = useState(1);
  const [isPending, startTransition] = useTransition();
  // Store the actual quantity in separate state to show the mismatch.
  const [clientQuantity, setClientQuantity] = useState(1);

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

    // Access the pending state of the transition,
    // by wrapping in startTransition again.
    startTransition(async () => {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  };

  return (
    <div>
      <h1>Checkout</h1>
      <Item action={updateQuantityAction}/>
      <hr />
      <Total clientQuantity={clientQuantity} savedQuantity={quantity} isPending={isPending} />
    </div>
  );
}

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.

This is expected, because Actions within a Transition do not guarantee execution order. For common use cases, React provides higher-level abstractions like useActionState and <form> actions that handle ordering for you. For advanced use cases, you’ll need to implement your own queuing and abort logic to handle this.