본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -useTransition

- useTransition : UI를 차단하지 않고 state를 업데이트할 수 있는 React 훅

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

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

- useTransition이 반환하는 startTransition 함수를 사용하면 state 업데이트를 트랜지션으로 표시할 수 있다.

- useTransition은 훅이므로 컴포넌트나 커스텀 훅 내부에서만 호출할 수 있다. 만약 다른 곳(데이터 라이브러리)에서 트랜지션을 시작해야하는 경우, 독립형 startTransition을 호출해야 한다.(대신 isPending 표시기를 제공하지 않는다.)

// setTransitino에 전달하는 비동기식 함수
startTransition(() => {
  // ❌ Setting state *after* startTransition call
  setTimeout(() => {
    setPage('/about');
  }, 1000);
});

// setTransition에 전달하는 동기식 함수
setTimeout(() => {
  startTransition(() => {
    // ✅ Setting state *during* startTransition call
    setPage('/about');
  });
}, 1000);
import { startTransition } from 'react';

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

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

- 해당 state의 set 함수에 접근할 수 있는 경우에만 업데이트를 트랜지션으로 감쌀 수 있다. 만약 일부 prop이나 커스텀 훅 값에 대한 응답으로 트랜지션을 시작하려면, useDeferredValue를 사용해야 한다.


Marking a state update as a non-blocking transtion(state 업데이트를 논블로킹 트랜지션으로 표시하기)

- 트랜지션을 사용하면 느린 디바이스에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있다.

- 트랜지션을 사용하면 리렌더링 도중에도 UI가 반응성을 유지한다.

- 예를들어, 사용자가 A 탭을 클릭했다가, A 탭이 리렌더링되기 전에 B 탭을 클릭하면 A 탭의 리렌더링이 완료될때 까지 기다리지 않고, B 탭을 리렌더링 한다.

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

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

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);      
    });
  }
  // 트랜지션 없이 현재 탭 업데이트하기
  function selectTab(nextTab) {
    setTab(nextTab);
  }

  return (
    <>
      <TabButton
        isActive={tab === 'about'}
        onClick={() => selectTab('about')}
      >
        About
      </TabButton>
      <TabButton
        isActive={tab === 'posts'}
        onClick={() => selectTab('posts')}
      >
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}

- 위 예제는 "Posts"탭이 인위적으로 느려지도록 하여 렌더링하는 데 최소 1초가 걸리도록 되어있다.

- setTransition을 사용하지 않는 경우에는, Posts 탭을 누르면 사용자 인터페이스가 정지된다.

- 반면, setTransition을 사용하면, Posts 탭을 누르더라도 사용자 인터페이스가 정지되지 않는다.


- suspense를 사용할 때, About, Posts, Contatc 버튼이 사라지고 스피너가 나타나는 UI를 방지하기 위해, TabButton에 useTransition을 추가해서 탭 버튼에 isPending state를 표시할 수 있다.

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

- 그렇다면, 스피너를 사용하지 않는 경우에는 suspense를 굳이 사용하는 이유가 있을까?(내일 스터디 질문)

- 기본적으로 suspense의 fallback을 사용하되, 사용자 경험을 유지하고 싶을 때는 useTransition 등을 사용하는 것으로


Troubleshooting(문제해결)

Updating an input in a transition doesn't work(트랜지션에서 input 업데이트가 작동하지 않습니다)

- input을 제어하는 state 변수에는 트랜지션을 사용할 수 없다.

- 트랜지션은 논블로킹이지만, 변경 이벤트에 대한 응답으로 input을 업데이트하는 것은 동기적으로 이루어져야하기 때문이다.

 

React doesn't treat my state update as a transition(React가 state 업데이트를 트랜지션으로 처리하지 않습니다.)

- startTransition에 전달하는 함수는 동기식이어야 한다.


StartTransition에 전달한 함수는 즉시 실행된다.

- React는 함수를 즉시 실행하지만, 함수가 실행되는 동안 예약된 모든 state 업데이트는 트랜지션으로 표시된다.

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

- 위 예제에서 콘솔창에 1, 2, 3 순서로 출력된다.

- 즉, startTransition에 전달한 함수는 지연되지 않는다.


* 참고 : React 공식문서(https://react-ko.dev/learn)