UI를 막지 않고 state를 업데이트할 수 있는 React Hook
const [isPending, startTransition] = useTransition()
컴포넌트의 최상위 수준에서 useTransition을 호출하여 일부 state 업데이트를 트랜지션으로 표시:
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
parameter를 받지 않음.
useTransition은 정확히 두 개의 항목이 있는 배열을 반환함:
isPending 플래그.startTransition 함수.useTransition에서 반환하는 startTransition 함수를 사용하면 state 업데이트를 transition으로 표시할 수 있음.
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
scope: 하나 이상의 set 함수를 호출하여 일부 state를 업데이트하는 함수. React는 매개변수 없이 즉시 scope를 호출하고 scope 함수를 호출하는 동안 동기적으로 예약된 모든 state 업데이트를 transition으로 표시함. 이러한 transition은 blocking되지 않으며, 원치 않는 loading indicator를 표시하지 않음.아무것도 반환하지 않음.
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을 일괄 처리함. 이는 향후의 릴리즈에서 제거될 가능성이 높은 제한 사항임.
컴포넌트의 최상위 수준에서 useTransition을 호출하여 state 업데이트를 non-blocking transition으로 표시할 수 있음.
import { useState, useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
useTransition은 정확히 두 개의 항목이 있는 배열을 반환함:
isPending 플래그.startTransition 함수.그런 다음, 다음과 같이 state 업데이트를 transition으로 표시할 수 있음:
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ...
}
Transition을 사용하면 느린 기기에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있음.
트랜지션을 사용하면 다시 렌더링하는 동안에도 UI의 반응성이 유지됨. 예를 들어, 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하면 첫 번째 리렌더링이 완료될 때까지 기다릴 필요 없이 다른 탭을 클릭할 수 있음.
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으로 표시되므로 사용자 상호 작용을 차단하지 않음.
useTransition이 반환하는 isPending boolean 값을 사용하여 transition이 진행 중임을 사용자에게 표시할 수 있음. 예를 들어, 탭 버튼은 특별한 시각적 'pending' state를 가질 수 있음:
function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
// ...
if (isPending) {
return <b className="pending">{children}</b>;
}
// ...
이를 이용해 UI 자체를 바로 업데이트해서 반응성을 높일 수 있음.
아래 예제에서 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가 어색해짐. 대신 TabButton에 useTransition을 추가하면 탭 버튼에 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은 이를 '대기'하지 않음.
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으로 감쌀 것으로 기대됨.
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>
);
}
입력을 제어하는 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을 실행하려면 두 가지 옵션이 있음:
useDeferredValue를 추가할 수 있음. 그러면 non-blocking 리렌더가 새 값을 자동으로 "따라잡기" 위해 트리거됨.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');
});
Hook이기 때문에 컴포넌트 외부에서 useTransition을 호출할 수 없음. 이 경우 대신 독립형 startTransition 메서드를 사용할 수 있음. 같은 방식으로 작동하지만 isPending indicator를 제공하지 않음.
다음 코드를 실행하면 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 ...
}
}