The Correct Way to Write useTransition Hook in React

July 15, 2023 (1y ago)

React 18 introduced several powerful features that changed how we think about rendering and state updates. One of the most useful additions is the useTransition hook, which allows developers to mark certain state updates as "transitions" - lower priority updates that can be interrupted by more urgent updates.

What Is useTransition?

The useTransition hook provides a way to tell React which state updates are less urgent, allowing the browser to work on more critical updates first. It returns a tuple containing:

  1. A boolean isPending flag that tells you if a transition is in progress
  2. A startTransition function that lets you mark updates as transitions
import { useTransition } from 'react';
 
function MyComponent() {
  const [isPending, startTransition] = useTransition();
  // ...
}

Common Mistakes When Using useTransition

Before diving into best practices, let's identify common mistakes:

Mistake 1: Using it for every state update

// ❌ Don't do this
const [isPending, startTransition] = useTransition();
const handleClick = () => {
  startTransition(() => {
    // Simple, fast state update that doesn't need to be a transition
    setCounter(counter + 1);
  });
};

Mistake 2: Using it outside component scope

// ❌ Don't do this - outside component scope
const [isPending, startTransition] = useTransition(); // Error: Hooks must be called in component body
 
function handleSomeEvent() {
  // ...
}

Mistake 3: Using it for asynchronous operations directly

// ❌ Misunderstanding what useTransition does
startTransition(async () => {
  const data = await fetchSomeData();
  setData(data); // Only this state update is marked as transition, not the fetch itself
});

The Correct Way to Use useTransition

1. Import and Initialize Properly

import { useState, useTransition } from 'react';
 
function MyComponent() {
  const [isPending, startTransition] = useTransition();
  const [searchResults, setSearchResults] = useState([]);
  const [searchQuery, setSearchQuery] = useState('');
  
  // ...
}

2. Use for UI Updates That Might Be Computationally Expensive

const handleSearch = (query) => {
  // Immediate update to show user feedback
  setSearchQuery(query);
  
  // Wrap expensive update in startTransition
  startTransition(() => {
    // This update can be interrupted if needed
    setSearchResults(filterLargeDataset(query));
  });
};

3. Show Loading Indicators When Appropriate

return (
  <div>
    <input 
      type="text" 
      value={searchQuery} 
      onChange={(e) => handleSearch(e.target.value)} 
    />
    
    {isPending ? (
      <p>Updating results...</p>
    ) : (
      <ResultsList results={searchResults} />
    )}
  </div>
);

4. Combine with useDeferredValue When Appropriate

Sometimes, you might want to combine useTransition with useDeferredValue for derived values:

import { useState, useTransition, useDeferredValue } from 'react';
 
function SearchComponent({ allItems }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const [isPending, startTransition] = useTransition();
  
  // Filtering happens based on the deferred value
  const filteredItems = allItems.filter(item => 
    item.name.toLowerCase().includes(deferredQuery.toLowerCase())
  );
  
  return (
    <div>
      <input 
        value={query}
        onChange={(e) => {
          setQuery(e.target.value);
        }}
      />
      
      {isPending && <div>Loading...</div>}
      
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

Real-World Example: Implementing a Search Feature

Let's look at a complete example of using useTransition in a search component:

import React, { useState, useTransition } from 'react';
 
function SearchableList({ items }) {
  const [query, setQuery] = useState('');
  const [filteredList, setFilteredList] = useState(items);
  const [isPending, startTransition] = useTransition();
  
  const handleSearch = (e) => {
    // Update the input field immediately - high priority
    const newQuery = e.target.value;
    setQuery(newQuery);
    
    // Mark the filtering operation as a transition (lower priority)
    startTransition(() => {
      // This expensive filtering operation can be interrupted
      if (newQuery.trim() === '') {
        setFilteredList(items);
      } else {
        const filtered = items.filter(item =>
          item.toLowerCase().includes(newQuery.toLowerCase())
        );
        setFilteredList(filtered);
      }
    });
  };
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleSearch}
        placeholder="Search items..."
      />
      
      {isPending ? (
        <p>Updating results...</p>
      ) : (
        <ul>
          {filteredList.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

When to Use useTransition

Use useTransition when:

  1. You're updating state that might cause expensive rendering work
  2. You want to prioritize certain UI updates over others
  3. You want to keep the UI responsive during expensive operations
  4. You're filtering or processing large datasets based on user input

Don't use useTransition when:

  1. The state update is critical and should happen immediately
  2. The operation is already fast and doesn't cause UI jank
  3. The operation is truly asynchronous (like data fetching) - consider React Query or SWR instead

Conclusion

The useTransition hook is a powerful tool for improving user experience by prioritizing UI updates. When used correctly, it can make your React applications feel more responsive, even during expensive operations.

Remember the key principles:

By following these guidelines, you'll be using the useTransition hook the right way and creating smoother experiences for your users.