useTransition

Chaerin Kim·2023년 12월 4일

UI를 막지 않고 state를 업데이트할 수 있는 React Hook

const [isPending, startTransition] = useTransition()

Reference

useTransition()

컴포넌트의 최상위 수준에서 useTransition을 호출하여 일부 state 업데이트를 트랜지션으로 표시:

import { useTransition } from 'react';

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

Parameters

parameter를 받지 않음.

Returns

useTransition은 정확히 두 개의 항목이 있는 배열을 반환함:

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

startTransition function

useTransition에서 반환하는 startTransition 함수를 사용하면 state 업데이트를 transition으로 표시할 수 있음.

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

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

Parameters

Returns

아무것도 반환하지 않음.

Caveats

  • useTransition은 Hook이므로 컴포넌트나 사용자 정의 Hook 내부에서만 호출할 수 있음. 다른 곳(예: 데이터 라이브러리)에서 transition을 시작해야 하는 경우, 대신 독립형 startTransition을 호출할 것.

  • 해당 state의 set 함수에 액세스할 수 있는 경우에만 update를 transition으로 래핑할 수 있음. 일부 prop이나 사용자 정의 Hook 값에 대한 응답으로 transition을 시작하려면 대신 useDeferredValue를 사용할 것.

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

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

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

  • 진행 중인 transition이 여러 개 있는 경우 React는 transition을 일괄 처리함. 이는 향후의 릴리즈에서 제거될 가능성이 높은 제한 사항임.


Usage

Marking a state update as a non-blocking transition

컴포넌트의 최상위 수준에서 useTransition을 호출하여 state 업데이트를 non-blocking transition으로 표시할 수 있음.

import { useState, useTransition } from 'react';

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

useTransition은 정확히 두 개의 항목이 있는 배열을 반환함:

  1. 보류 중인 transition이 있는지 여부를 알려주는 isPending 플래그.
  2. State 업데이트를 transition으로 표시할 수 있는 startTransition 함수.

그런 다음, 다음과 같이 state 업데이트를 transition으로 표시할 수 있음:

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

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

Transition을 사용하면 느린 기기에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있음.

트랜지션을 사용하면 다시 렌더링하는 동안에도 UI의 반응성이 유지됨. 예를 들어, 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하면 첫 번째 리렌더링이 완료될 때까지 기다릴 필요 없이 다른 탭을 클릭할 수 있음.

Updating the parent component in a transition

useTransition 호출에서 부모 컴포넌트의 state를 업데이트할 수도 있음. 예를 들어, 이 TabButton 컴포넌트는 onClick 로직을 transition으로 래핑함:

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

부모 컴포넌트는 onClick 이벤트 핸들러 내에서 state를 업데이트하므로 해당 state 업데이트는 transition으로 표시됨. 선택한 탭을 업데이트하는 것은 transition으로 표시되므로 사용자 상호 작용을 차단하지 않음.

Displaying a pending visual state during the transition

useTransition이 반환하는 isPending boolean 값을 사용하여 transition이 진행 중임을 사용자에게 표시할 수 있음. 예를 들어, 탭 버튼은 특별한 시각적 'pending' state를 가질 수 있음:

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

이를 이용해 UI 자체를 바로 업데이트해서 반응성을 높일 수 있음.

Preventing unwanted loading indicators

아래 예제에서 PostsTab 컴포넌트는 Suspnse-enabled한 데이터 소스를 사용하여 일부 데이터를 가져옴. "Posts" 탭을 클릭하면 PostsTab 컴포넌트가 suspend되어 가장 가까운 loading fallback이 표시됨:

// App.js
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'}
        onClick={() => setTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => setTab('posts')}
      >
        Posts
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => setTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </Suspense>
  );
}
// TabButton.js
export default function TabButton({ children, isActive, onClick }) {
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      onClick();
    }}>
      {children}
    </button>
  );
}

Loading indicatior를 표시하기 위해 전체 탭 컨테이너를 숨기면 UX가 어색해짐. 대신 TabButtonuseTransition을 추가하면 탭 버튼에 pending state를 표시할 수 있음.

'Posts'를 클릭하면 더 이상 전체 탭 컨테이너가 spinner로 대체되지 않음:

// TabButton.js
import { useTransition } from 'react';

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

Note

Transition은 이미 표시된 콘텐츠(예: 탭 컨테이너)를 숨기지 않을 만큼만 '대기'함. Posts 탭에 중첩된 <Suspense> 경계가 있는 경우 transition은 이를 '대기'하지 않음.

Building a Suspense-enabled router

React 프레임워크나 라우터를 구축하는 경우 페이지 네비게이션을 transition으로 표시하는 것이 좋음.

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

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

이 방법이 권장되는 두 가지 이유:

네비게이션에 transition을 사용하는 아주 간단한 라우터 예시:

// App.js
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>;
}

Note

Suspense-enabled한 라우터는 기본적으로 네비게이션 업데이트를 transition으로 감쌀 것으로 기대됨.

Displaying an error to users with a error boundary

Canary

useTransition의 Error Boundary는 현재 React의 canary 및 실험 채널에서만 사용할 수 있음.

참고: React의 릴리즈 채널

startTransition에 전달된 함수가 오류를 발생시키면 error boundary를 사용하여 사용자에게 오류를 표시할 수 있음. Error boundary를 사용하려면 useTransition을 호출하는 컴포넌트를 error boundary로 감싸면 됨. startTransition에 전달된 함수가 에러를 발생시키면 error boundary에 대한 fallback이 표시됨.

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 Error('Example error')
  }
}

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

Updating an input in a transition doesn’t work

입력을 제어하는 state 변수에는 transition을 사용할 수 없음:

const [text, setText] = useState('');

// ...

function handleChange(e) {
  // ❌ Can't use transitions for controlled input state
  startTransition(() => {
    setText(e.target.value);
  });
}

// ...

return <input value={text} onChange={handleChange} />;

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

  1. 입력 상태(항상 동기적으로 업데이트됨)에 대한 state와 transition 시 업데이트할 state 변수를 각각 선언할 수 있음. 이렇게 하면 동기식 state를 사용하여 input을 제어하고, 나머지 렌더링 로직에 (입력보다 "지연되는") transition state 변수를 전달할 수 있음.
  2. 또는 state 변수가 하나만 있고 실제 값보다 "지연"되는 useDeferredValue를 추가할 수 있음. 그러면 non-blocking 리렌더가 새 값을 자동으로 "따라잡기" 위해 트리거됨.

React doesn’t treat my state update as a transition

state 업데이트를 transition으로 감쌀 때는 startTransition 호출 '중'에 업데이트가 발생하도록 할 것:

startTransition(() => {
  // ✅ Setting state *during* startTransition call
  setPage('/about');
});

startTransition에 전달하는 함수는 동기적이어야 함.

다음와 같은 업데이트를 transition으로 표시할 수 없음:

startTransition(() => {
  // ❌ Setting state *after* startTransition call
  setTimeout(() => {
    setPage('/about');
  }, 1000);
});

대신, 다음과 같이 할 수 있음:

setTimeout(() => {
  startTransition(() => {
    // ✅ Setting state *during* startTransition call
    setPage('/about');
  });
}, 1000);

마찬가지로, 다음과 같은 업데이트를 transition으로 표시할 수 없음:

startTransition(async () => {
  await someAsyncFunction();
  // ❌ Setting state *after* startTransition call
  setPage('/about');
});

대신, 다음과 같이 할 수 있음:

await someAsyncFunction();
startTransition(() => {
  // ✅ Setting state *during* startTransition call
  setPage('/about');
});

I want to call useTransition from outside a component

Hook이기 때문에 컴포넌트 외부에서 useTransition을 호출할 수 없음. 이 경우 대신 독립형 startTransition 메서드를 사용할 수 있음. 같은 방식으로 작동하지만 isPending indicator를 제공하지 않음.

The function I pass to startTransition executes immediately

다음 코드를 실행하면 1, 2, 3이 출력됨:

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

startTransition에 전달하는 함수는 지연되지 않음. 브라우저 setTimeout과 달리 나중에 callback을 실행하지 않음. React는 함수를 즉시 실행하지만, 함수가 실행되는 동안 예약된 모든 state 업데이트는 transition으로 표시됨. 다음과 같이 작동한다고 상상할 수 있음:

// A simplified version of how React works

let isInsideTransition = false;

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

function setState() {
  if (isInsideTransition) {
    
    // ... schedule a transition state update ...
    
  } else {
    
    // ... schedule an urgent state update ...
    
  }
}

0개의 댓글