← Назад к вопросам

Для чего нужен useTransition?

1.0 Junior🔥 171 комментариев
#React#Оптимизация и производительность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Для чего нужен useTransition?

useTransition - это React хук (введён в React 18), который позволяет отметить обновление состояния как неблокирующее (non-urgent). Хук предоставляет способ приоритизировать обновления: срочные обновления (клики, вводы) получают высокий приоритет, а менее срочные обновления (асинхронные операции, рендеринг больших списков) получают низкий приоритет. Это значительно улучшает отзывчивость приложения.

Проблема, которую решает useTransition

Блокирующие обновления замораживают интерфейс

import { useState } from 'react';

function SearchUsers() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value); // Это синхронное обновление

    // Если results содержит 10000 элементов, рендеринг блокирует UI
    const filtered = mockDatabase.filter(user =>
      user.name.includes(value)
    );
    setResults(filtered);
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {/* Во время рендеринга большого списка input становится неотзывчивым! */}
      <ul>
        {results.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}

Проблема: если список результатов большой, рендеринг может занять несколько сотен миллисекунд, и пользователь будет чувствовать "зависание" при вводе.

Как работает useTransition

import { useState, useTransition } from 'react';

function SearchUsers() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    
    // Это обновление срочное - выполняется немедленно
    setQuery(value);

    // Это обновление неблокирующее - может быть прервано
    startTransition(() => {
      const filtered = mockDatabase.filter(user =>
        user.name.includes(value)
      );
      setResults(filtered);
    });
  };

  return (
    <div>
      <input
        value={query}
        onChange={handleChange}
        placeholder="Search users..."
      />
      {/* Показываем spinner пока идёт обновление */}
      {isPending && <p>Loading results...</p>}
      <ul>
        {results.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}

Теперь при вводе:

  1. Input обновляется сразу (высокий приоритет)
  2. Фильтрация результатов происходит в фоне (низкий приоритет)
  3. UI остаётся отзывчивым
  4. isPending указывает, что обновление в процессе

Структура useTransition

Хук возвращает массив с двумя элементами:

const [isPending, startTransition] = useTransition();

// isPending (boolean) - true когда transition обновление в процессе
// startTransition (function) - функция для запуска transition обновления

Практический пример 1: Поиск в большом списке

import { useState, useTransition } from 'react';

function UserDirectory() {
  const [input, setInput] = useState('');
  const [deferredValue, setDeferredValue] = useState('');
  const [isPending, startTransition] = useTransition();

  const handleInputChange = (e) => {
    const value = e.target.value;
    // Срочное обновление - input реагирует сразу
    setInput(value);

    // Отложенное обновление - поиск происходит в фоне
    startTransition(() => {
      setDeferredValue(value);
    });
  };

  // Фильтруем по отложенному значению
  const filteredUsers = useMemo(() => {
    console.log('Filtering users...');
    return allUsers.filter(user =>
      user.name.toLowerCase().includes(deferredValue.toLowerCase())
    );
  }, [deferredValue]);

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={handleInputChange}
        placeholder="Search users..."
      />
      {isPending && <div className="spinner">Searching...</div>}
      <div className={isPending ? 'opacity-50' : ''}>
        {filteredUsers.map(user => (
          <div key={user.id} className="user-card">
            <h3>{user.name}</h3>
            <p>{user.email}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Практический пример 2: Переход между вкладками с большим содержимым

import { useState, useTransition } from 'react';

function Dashboard() {
  const [activeTab, setActiveTab] = useState('overview');
  const [isPending, startTransition] = useTransition();

  const handleTabClick = (tabName) => {
    // Старая вкладка остаётся видимой, пока новая рендерится
    startTransition(() => {
      setActiveTab(tabName);
    });
  };

  return (
    <div>
      <div className="tabs">
        <button
          onClick={() => handleTabClick('overview')}
          className={activeTab === 'overview' ? 'active' : ''}
          disabled={isPending}
        >
          Overview
        </button>
        <button
          onClick={() => handleTabClick('analytics')}
          className={activeTab === 'analytics' ? 'active' : ''}
          disabled={isPending}
        >
          Analytics
        </button>
        <button
          onClick={() => handleTabClick('settings')}
          className={activeTab === 'settings' ? 'active' : ''}
          disabled={isPending}
        >
          Settings
        </button>
      </div>

      <div className="tab-content">
        {isPending && (
          <div className="loading-overlay">Loading tab...</div>
        )}
        {activeTab === 'overview' && <OverviewTab />}
        {activeTab === 'analytics' && <AnalyticsTab />}
        {activeTab === 'settings' && <SettingsTab />}
      </div>
    </div>
  );
}

Практический пример 3: Сортировка большого списка

import { useState, useTransition, useMemo } from 'react';

function ProductList({ products }) {
  const [sortBy, setSortBy] = useState('name');
  const [isPending, startTransition] = useTransition();

  const handleSort = (sortField) => {
    startTransition(() => {
      setSortBy(sortField);
    });
  };

  const sortedProducts = useMemo(() => {
    // Это может быть дорогостоящая операция
    const copy = [...products];
    if (sortBy === 'name') {
      return copy.sort((a, b) => a.name.localeCompare(b.name));
    } else if (sortBy === 'price') {
      return copy.sort((a, b) => a.price - b.price);
    } else if (sortBy === 'rating') {
      return copy.sort((a, b) => b.rating - a.rating);
    }
    return copy;
  }, [sortBy]);

  return (
    <div>
      <div className="sort-controls">
        <button
          onClick={() => handleSort('name')}
          className={sortBy === 'name' ? 'active' : ''}
          disabled={isPending}
        >
          Sort by Name
        </button>
        <button
          onClick={() => handleSort('price')}
          className={sortBy === 'price' ? 'active' : ''}
          disabled={isPending}
        >
          Sort by Price
        </button>
        <button
          onClick={() => handleSort('rating')}
          className={sortBy === 'rating' ? 'active' : ''}
          disabled={isPending}
        >
          Sort by Rating
        </button>
      </div>

      <div className={isPending ? 'updating' : ''}>
        {sortedProducts.map(product => (
          <div key={product.id} className="product-card">
            <h3>{product.name}</h3>
            <p>Price: ${product.price}</p>
            <p>Rating: {product.rating}/5</p>
          </div>
        ))}
      </div>
    </div>
  );
}

useTransition vs useDeferredValue

Eсть похожий хук useDeferredValue для отложенного обновления значения:

// useTransition - когда ты контролируешь setState
const [isPending, startTransition] = useTransition();
startTransition(() => setState(newValue));

// useDeferredValue - когда ты получаешь значение как prop
const deferredValue = useDeferredValue(value);
// дефолтный изоляционный способ

Пример с useDeferredValue

import { useState, useDeferredValue } from 'react';

function App() {
  const [input, setInput] = useState('');
  const deferredInput = useDeferredValue(input);

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      {/* Результаты будут отображаться с задержкой */}
      <SearchResults query={deferredInput} />
    </div>
  );
}

Когда использовать useTransition

  1. Поиск/фильтрация в больших списках - позволяет input оставаться отзывчивым
  2. Переключение между вкладками с тяжёлым содержимым
  3. Сортировка/группировка данных которые требуют перерендеринга
  4. Асинхронные операции которые не требуют срочного обновления
  5. Формы где один input должен быть быстрым, а остальное может быть отложено

Важные замечания

// Обновления внутри startTransition становятся неблокирующими
startTransition(() => {
  // Эти setState вызовы получают низкий приоритет
  setFilteredResults(filtered);
  setSearchMetadata(metadata);
});

// React может прервать transition если придёт срочное обновление
// Это нормальное поведение - UI остаётся отзывчивым

// Состояние isPending обновляется, когда transition начинается/заканчивается
if (isPending) {
  // Можно показать loading indicator, уменьшить opacity и т.д.
}

useTransition - это мощный инструмент для оптимизации производительности в React 18+, особенно полезный для больших приложений с интенсивным рендерингом.

Для чего нужен useTransition? | PrepBro