Skip to main content
useTransition is a React Hook that lets you update the state without blocking the UI.
function useTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void
]

Parameters

useTransition does not take any parameters.

Returns

useTransition returns an array with exactly two values:
[0]
boolean
The isPending flag that tells you whether there is a pending transition.
[1]
(callback: () => void, options?: StartTransitionOptions) => void
The startTransition function that lets you mark a state update as a transition.
  • callback: A function that updates state. React will immediately call this function, marking all state updates scheduled synchronously during this function call as transitions.
  • options: Optional object with a name property (string) for debugging in React DevTools.

Usage

Marking a state update as a non-blocking transition

Call useTransition at the top level of your component to mark state updates as transitions:
import { useState, useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');
  
  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  
  return (
    <>
      <button onClick={() => selectTab('about')}>
        About {tab === 'about' && isPending && '(Loading...)'}
      </button>
      <button onClick={() => selectTab('posts')}>
        Posts {tab === 'posts' && isPending && '(Loading...)'}
      </button>
      <button onClick={() => selectTab('contact')}>
        Contact {tab === 'contact' && isPending && '(Loading...)'}
      </button>
      
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}

Updating the parent component in a transition

You can call startTransition from a child component to update parent state:
function TabButton({ children, onClick }) {
  const [isPending, startTransition] = useTransition();
  
  function handleClick() {
    startTransition(() => {
      onClick();
    });
  }
  
  return (
    <button onClick={handleClick} disabled={isPending}>
      {children}
    </button>
  );
}

function TabContainer() {
  const [tab, setTab] = useState('about');
  
  return (
    <>
      <TabButton onClick={() => setTab('about')}>About</TabButton>
      <TabButton onClick={() => setTab('posts')}>Posts</TabButton>
      <TabButton onClick={() => setTab('contact')}>Contact</TabButton>
      
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}

Displaying a pending visual state during the transition

Show loading indicators while the transition is pending:
function App() {
  const [isPending, startTransition] = useTransition();
  const [page, setPage] = useState('home');
  
  function navigate(nextPage) {
    startTransition(() => {
      setPage(nextPage);
    });
  }
  
  return (
    <div style={{ opacity: isPending ? 0.7 : 1 }}>
      <nav>
        <button onClick={() => navigate('home')}>Home</button>
        <button onClick={() => navigate('about')}>About</button>
        <button onClick={() => navigate('products')}>Products</button>
      </nav>
      
      {isPending && <Spinner />}
      
      {page === 'home' && <HomePage />}
      {page === 'about' && <AboutPage />}
      {page === 'products' && <ProductsPage />}
    </div>
  );
}

Preventing unwanted loading indicators

In this example, the PostsTab component fetches data using Suspense. When you click the “Posts” tab, it immediately shows a pending state:
function PostsTab() {
  const posts = use(fetchPosts());
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');
  
  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  
  return (
    <Suspense fallback={<h1>Loading...</h1>}>
      <button onClick={() => selectTab('about')}>About</button>
      <button onClick={() => selectTab('posts')}>Posts</button>
      
      {isPending ? (
        <div>Loading tab...</div>
      ) : (
        <>
          {tab === 'about' && <AboutTab />}
          {tab === 'posts' && <PostsTab />}
        </>
      )}
    </Suspense>
  );
}

Common Patterns

Search with transitions

function SearchPage() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  function handleSearch(value) {
    setQuery(value); // Urgent: update input
    startTransition(() => {
      // Non-urgent: update results
      setResults(searchItems(value));
    });
  }
  
  return (
    <>
      <input
        value={query}
        onChange={e => handleSearch(e.target.value)}
      />
      {isPending && <div>Searching...</div>}
      <SearchResults results={results} />
    </>
  );
}
function Router() {
  const [isPending, startTransition] = useTransition();
  const [currentPage, setCurrentPage] = useState('/');
  
  function navigate(url) {
    startTransition(() => {
      setCurrentPage(url);
    });
  }
  
  return (
    <div className={isPending ? 'loading' : ''}>
      <nav>
        <a href="/" onClick={e => { e.preventDefault(); navigate('/'); }}>
          Home
        </a>
        <a href="/about" onClick={e => { e.preventDefault(); navigate('/about'); }}>
          About
        </a>
      </nav>
      
      {isPending && <LoadingBar />}
      
      <main>
        {currentPage === '/' && <HomePage />}
        {currentPage === '/about' && <AboutPage />}
      </main>
    </div>
  );
}

Form submission with transitions

function ContactForm() {
  const [isPending, startTransition] = useTransition();
  const [status, setStatus] = useState('idle');
  
  function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    
    startTransition(async () => {
      try {
        await submitForm(formData);
        setStatus('success');
      } catch (error) {
        setStatus('error');
      }
    });
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />
      
      <button type="submit" disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
      
      {status === 'success' && <div>Form submitted!</div>}
      {status === 'error' && <div>Error submitting form</div>}
    </form>
  );
}

Filtering with transitions

function ProductList({ products }) {
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState('');
  const [filteredProducts, setFilteredProducts] = useState(products);
  
  function handleFilterChange(value) {
    setFilter(value); // Urgent: update input
    
    startTransition(() => {
      // Non-urgent: filter products
      const filtered = products.filter(p =>
        p.name.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredProducts(filtered);
    });
  }
  
  return (
    <>
      <input
        value={filter}
        onChange={e => handleFilterChange(e.target.value)}
        placeholder="Filter products..."
      />
      
      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        {filteredProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </>
  );
}

TypeScript

import { useState, useTransition } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState<'about' | 'posts' | 'contact'>('about');
  
  function selectTab(nextTab: 'about' | 'posts' | 'contact') {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  
  return (
    <>
      <button onClick={() => selectTab('about')}>About</button>
      <button onClick={() => selectTab('posts')}>Posts</button>
      <button onClick={() => selectTab('contact')}>Contact</button>
    </>
  );
}

// With options
function Component() {
  const [isPending, startTransition] = useTransition();
  
  function handleUpdate() {
    startTransition(() => {
      // State updates
    }, { name: 'update-transition' });
  }
}

Troubleshooting

Updating an input in a transition doesn’t work

You can’t use transitions for state that controls an input:
// ❌ Input feels sluggish
function SearchBox() {
  const [isPending, startTransition] = useTransition();
  const [text, setText] = useState('');
  
  function handleChange(e) {
    startTransition(() => {
      setText(e.target.value); // Don't defer input updates!
    });
  }
  
  return <input value={text} onChange={handleChange} />;
}

// ✅ Keep input responsive
function SearchBox() {
  const [isPending, startTransition] = useTransition();
  const [text, setText] = useState('');
  const [deferredText, setDeferredText] = useState('');
  
  function handleChange(e) {
    setText(e.target.value); // Urgent: update input
    startTransition(() => {
      setDeferredText(e.target.value); // Non-urgent: update results
    });
  }
  
  return (
    <>
      <input value={text} onChange={handleChange} />
      <Results query={deferredText} />
    </>
  );
}

React doesn’t treat my state update as a transition

Make sure the state update is inside the startTransition callback:
// ❌ State update outside transition
startTransition(() => {
  // Some sync work
});
setState(newValue); // Not in transition!

// ✅ State update inside transition
startTransition(() => {
  setTab(nextTab);
});

My transition never finishes

Transitions that involve async work might not show as pending:
// ❌ Async setState not tracked
startTransition(async () => {
  const data = await fetchData();
  setData(data); // isPending is already false
});

// ✅ Track async work separately
const [isLoading, setIsLoading] = useState(false);

startTransition(() => {
  setIsLoading(true);
});

fetchData().then(data => {
  startTransition(() => {
    setData(data);
    setIsLoading(false);
  });
});

useTransition vs useDeferredValue

FeatureuseTransitionuseDeferredValue
ControlYou trigger updatesReact defers values
Use caseWhen you own the stateWhen you receive props
Pending stateisPending flagCompare values
SyntaxWrap in startTransitionWrap value
// useTransition: You control the update
function Tabs() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('home');
  
  function switchTab(next) {
    startTransition(() => {
      setTab(next);
    });
  }
}

// useDeferredValue: Defer a prop or value
function Tabs({ tab }) {
  const deferredTab = useDeferredValue(tab);
  const isPending = tab !== deferredTab;
  
  return <TabContent tab={deferredTab} />;
}

Best Practices

Keep urgent updates outside transitions

function Component() {
  const [isPending, startTransition] = useTransition();
  const [input, setInput] = useState('');
  const [results, setResults] = useState([]);
  
  function handleChange(e) {
    setInput(e.target.value); // Urgent
    
    startTransition(() => {
      setResults(search(e.target.value)); // Non-urgent
    });
  }
}

Use meaningful loading states

function Component() {
  const [isPending, startTransition] = useTransition();
  
  return (
    <div>
      {isPending ? (
        <div>Loading new content...</div>
      ) : (
        <Content />
      )}
    </div>
  );
}

Combine with Suspense

function App() {
  const [isPending, startTransition] = useTransition();
  const [page, setPage] = useState('home');
  
  return (
    <Suspense fallback={<Spinner />}>
      {isPending && <div>Transitioning...</div>}
      {page === 'home' && <HomePage />}
      {page === 'profile' && <ProfilePage />}
    </Suspense>
  );
}