← React/React 18: конкурентный рендеринг и новые хуки#279 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksМаршрут: старт с нуля

React 18: конкурентный рендеринг и новые хуки

Что изменилось в React 18

React 18 — самый значимый релиз за несколько лет. Главное нововведение — конкурентный рендеринг (Concurrent Rendering). Это не отдельная функция, а фундаментальное изменение внутренней архитектуры, которое открывает возможности для новых API.

Как обновиться:

// React 17 и ниже:
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))

// React 18:
import ReactDOM from 'react-dom/client'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)

createRoot — обязательный переход. Без него новые возможности React 18 не работают.

Конкурентный рендеринг

В React 17 рендеринг был синхронным и непрерывным: начав обновление, React не мог прерваться, пока не закончит. Это приводило к "зависаниям" интерфейса при тяжёлых вычислениях.

В React 18 рендеринг может быть прерван, приостановлен и возобновлён. React может:

  • Начать рендеринг
  • Увидеть более приоритетное обновление (например, нажатие кнопки)
  • Прервать текущий рендеринг
  • Обработать приоритетное обновление
  • Вернуться к прерванному рендерингу
  • Пользователь получает мгновенный отклик на важные действия, даже если в фоне идёт тяжёлый рендеринг.

    startTransition: срочные vs несрочные обновления

    startTransition позволяет явно разделить обновления на срочные (требуют немедленного отклика) и несрочные (переходы):

    import { startTransition } from 'react'
    
    function SearchBox() {
      const [query, setQuery] = useState('')
      const [results, setResults] = useState([])
    
      function handleInput(e) {
        // Срочное: обновить поле ввода немедленно
        setQuery(e.target.value)
    
        // Несрочное: фильтрация может подождать
        startTransition(() => {
          setResults(filterItems(e.target.value))  // тяжёлая операция
        })
      }
    
      return (
        <>
          <input value={query} onChange={handleInput} />
          <ResultsList items={results} />
        </>
      )
    }

    Если пользователь продолжает печатать пока идёт фильтрация — React прерывает фильтрацию и сначала обновляет поле ввода.

    useTransition: с индикатором загрузки

    useTransition — хук-версия startTransition с флагом isPending:

    import { useTransition } from 'react'
    
    function TabPanel() {
      const [activeTab, setActiveTab] = useState('home')
      const [isPending, startTransition] = useTransition()
    
      function handleTabChange(tab) {
        startTransition(() => {
          setActiveTab(tab)  // переключение вкладок — несрочное
        })
      }
    
      return (
        <>
          <nav>
            {tabs.map(tab => (
              <button
                key={tab}
                onClick={() => handleTabChange(tab)}
                style={{ opacity: isPending ? 0.7 : 1 }}  // покажем, что идёт переход
              >
                {tab}
              </button>
            ))}
          </nav>
    
          {isPending ? <Spinner /> : <TabContent tab={activeTab} />}
        </>
      )
    }

    useDeferredValue: откладываем дорогой рендеринг

    useDeferredValue откладывает обновление значения — похоже на debounce, но умнее:

    import { useDeferredValue } from 'react'
    
    function SearchResults({ query }) {
      const deferredQuery = useDeferredValue(query)
      // deferredQuery обновится позже, когда React найдёт время
    
      // Пока deferredQuery !== query — показываем устаревшие данные с затуханием
      const isStale = deferredQuery !== query
    
      return (
        <div style={{ opacity: isStale ? 0.7 : 1 }}>
          <SlowList query={deferredQuery} />  {/* тяжёлый компонент */}
        </div>
      )
    }
    
    // В родителе — только срочное обновление input:
    function Search() {
      const [query, setQuery] = useState('')
    
      return (
        <>
          <input value={query} onChange={e => setQuery(e.target.value)} />
          <SearchResults query={query} />
        </>
      )
    }

    Автоматическая пакетная обработка (Automatic Batching)

    В React 17 обновления state внутри async-функций (setTimeout, fetch) не группировались:

    // React 17: 2 рендера
    setTimeout(() => {
      setCount(c => c + 1)  // рендер 1
      setFlag(f => !f)      // рендер 2
    }, 1000)
    
    // React 18: автоматически 1 рендер (batching везде!)
    setTimeout(() => {
      setCount(c => c + 1)  //   setFlag(f => !f)      // → один рендер
    }, 1000)

    Это бесплатное улучшение производительности без изменения кода.

    Strict Mode в React 18

    В React 18 Strict Mode намеренно монтирует, размонтирует и снова монтирует компоненты. Это помогает найти побочные эффекты, которые не очищаются правильно в useEffect.

    // В development режиме с Strict Mode:
    // useEffect вызовется дважды — чтобы проверить корректность cleanup
    useEffect(() => {
      const subscription = subscribe()
      return () => subscription.unsubscribe()  // должна работать правильно
    }, [])

    Примеры

    Демонстрация концепции срочных vs отложенных обновлений через requestAnimationFrame и setTimeout, имитация useTransition и useDeferredValue

    // Демонстрируем концепцию "конкурентных" обновлений через JS-примитивы.
    // В React это startTransition/useTransition/useDeferredValue.
    
    // --- Симуляция "тяжёлого" рендера ---
    
    function heavyComputation(query, itemCount = 5000) {
      const start = performance.now()
      const items = []
      // Имитация дорогой фильтрации большого списка
      for (let i = 0; i < itemCount; i++) {
        const item = 'Товар ' + i
        if (item.toLowerCase().includes(query.toLowerCase())) {
          items.push(item)
        }
      }
      const elapsed = Math.round(performance.now() - start)
      return { items: items.slice(0, 10), time: elapsed, total: items.length }
    }
    
    // --- Паттерн 1: Синхронный (React 17 стиль) ---
    // Каждое нажатие клавиши -> немедленный тяжёлый рендер
    
    function syncSearch(query) {
      const start = performance.now()
      const result = heavyComputation(query)
      const uiUpdateTime = Math.round(performance.now() - start)
    
      console.log('Синхронный поиск "' + query + '":')
      console.log('  UI заблокирован на:', uiUpdateTime + 'мс')
      console.log('  Найдено:', result.total, 'результатов')
      console.log('  ⚠️  Пользователь не может нажать кнопки во время поиска!')
      return result
    }
    
    // --- Паттерн 2: startTransition (React 18 стиль) ---
    // Срочное обновление (input) немедленно, тяжёлое (список) — позже
    
    function createTransitionSearch() {
      let pendingQuery = null
      let isPending = false
      let transitionId = 0
    
      function startTransition(callback) {
        isPending = true
        const currentId = ++transitionId
    
        // Переносим выполнение за пределы текущего стека вызовов
        // (в React это делается через планировщик — Scheduler)
        setTimeout(() => {
          if (currentId !== transitionId) {
            console.log('  [Transition] Прерван устаревший переход #' + currentId)
            return
          }
          callback()
          isPending = false
        }, 0)
      }
    
      function handleInput(query) {
        // 1. Срочное: обновить поле ввода НЕМЕДЛЕННО
        pendingQuery = query
        console.log('
    Ввод "' + query + '": поле обновлено мгновенно ✓')
        console.log('isPending:', true, '(идёт отложенное обновление)')
    
        // 2. Несрочное: тяжёлая фильтрация — через transition
        startTransition(() => {
          const result = heavyComputation(query)
          console.log('Transition завершён для "' + query + '":')
          console.log('  Найдено:', result.total, 'за', result.time + 'мс')
          console.log('  isPending:', false)
        })
      }
    
      return { handleInput, getIsPending: () => isPending }
    }
    
    // --- Паттерн 3: useDeferredValue (отложенное значение) ---
    
    function createDeferredValue(delay = 0) {
      let currentValue = ''
      let deferredValue = ''
      let deferredUpdateTimer = null
    
      function update(newValue) {
        currentValue = newValue
        const isStale = currentValue !== deferredValue
    
        console.log('
    Обновление: "' + newValue + '"')
        console.log('  currentValue:', currentValue, '(срочное, обновлено сразу)')
        console.log('  deferredValue:', deferredValue, '(устаревшее, пока не обновлено)')
        console.log('  isStale:', isStale)
    
        if (deferredUpdateTimer) clearTimeout(deferredUpdateTimer)
    
        // Откладываем обновление deferredValue
        deferredUpdateTimer = setTimeout(() => {
          deferredValue = currentValue
          const result = heavyComputation(deferredValue)
          console.log('  deferredValue обновлён: "' + deferredValue + '"')
          console.log('  Список перерендерен, найдено:', result.total)
        }, delay)
      }
    
      return { update }
    }
    
    // --- Демонстрация автоматического batching ---
    
    function demonstrateBatching() {
      console.log('
    === Автоматический Batching ===')
    
      let renderCount = 0
      const state = { count: 0, flag: false, name: '' }
    
      function setState(updates) {
        Object.assign(state, updates)
      }
    
      function scheduleRender() {
        renderCount++
        console.log('Рендер #' + renderCount, '| Состояние:', JSON.stringify(state))
      }
    
      // React 17: setTimeout без batching — 3 рендера
      console.log('React 17 (без batching в async):')
      // Симулируем отдельные рендеры
      setState({ count: state.count + 1 })
      scheduleRender()
      setState({ flag: !state.flag })
      scheduleRender()
      setState({ name: 'Алексей' })
      scheduleRender()
    
      // React 18: автоматический batching — 1 рендер
      console.log('
    React 18 (автоматический batching):')
      renderCount = 0
      // Все обновления сгруппированы в один рендер
      setState({ count: state.count + 1, flag: !state.flag, name: 'Мария' })
      scheduleRender()  // только один рендер!
      console.log('Рендеров:', renderCount, '(вместо 3)')
    }
    
    // --- Запуск демонстраций ---
    
    console.log('=== Синхронный поиск (React 17) ===')
    syncSearch('товар 10')
    
    console.log('
    === Конкурентный поиск (React 18) ===')
    const search = createTransitionSearch()
    search.handleInput('то')
    search.handleInput('тов')
    search.handleInput('товар')  // только этот transition завершится
    
    console.log('
    === useDeferredValue ===')
    const deferred = createDeferredValue(50)
    deferred.update('т')
    deferred.update('то')
    deferred.update('товар 5')  // быстрый ввод
    
    demonstrateBatching()

    React 18: конкурентный рендеринг и новые хуки

    Что изменилось в React 18

    React 18 — самый значимый релиз за несколько лет. Главное нововведение — конкурентный рендеринг (Concurrent Rendering). Это не отдельная функция, а фундаментальное изменение внутренней архитектуры, которое открывает возможности для новых API.

    Как обновиться:

    // React 17 и ниже:
    import ReactDOM from 'react-dom'
    ReactDOM.render(<App />, document.getElementById('root'))
    
    // React 18:
    import ReactDOM from 'react-dom/client'
    const root = ReactDOM.createRoot(document.getElementById('root'))
    root.render(<App />)

    createRoot — обязательный переход. Без него новые возможности React 18 не работают.

    Конкурентный рендеринг

    В React 17 рендеринг был синхронным и непрерывным: начав обновление, React не мог прерваться, пока не закончит. Это приводило к "зависаниям" интерфейса при тяжёлых вычислениях.

    В React 18 рендеринг может быть прерван, приостановлен и возобновлён. React может:

  • Начать рендеринг
  • Увидеть более приоритетное обновление (например, нажатие кнопки)
  • Прервать текущий рендеринг
  • Обработать приоритетное обновление
  • Вернуться к прерванному рендерингу
  • Пользователь получает мгновенный отклик на важные действия, даже если в фоне идёт тяжёлый рендеринг.

    startTransition: срочные vs несрочные обновления

    startTransition позволяет явно разделить обновления на срочные (требуют немедленного отклика) и несрочные (переходы):

    import { startTransition } from 'react'
    
    function SearchBox() {
      const [query, setQuery] = useState('')
      const [results, setResults] = useState([])
    
      function handleInput(e) {
        // Срочное: обновить поле ввода немедленно
        setQuery(e.target.value)
    
        // Несрочное: фильтрация может подождать
        startTransition(() => {
          setResults(filterItems(e.target.value))  // тяжёлая операция
        })
      }
    
      return (
        <>
          <input value={query} onChange={handleInput} />
          <ResultsList items={results} />
        </>
      )
    }

    Если пользователь продолжает печатать пока идёт фильтрация — React прерывает фильтрацию и сначала обновляет поле ввода.

    useTransition: с индикатором загрузки

    useTransition — хук-версия startTransition с флагом isPending:

    import { useTransition } from 'react'
    
    function TabPanel() {
      const [activeTab, setActiveTab] = useState('home')
      const [isPending, startTransition] = useTransition()
    
      function handleTabChange(tab) {
        startTransition(() => {
          setActiveTab(tab)  // переключение вкладок — несрочное
        })
      }
    
      return (
        <>
          <nav>
            {tabs.map(tab => (
              <button
                key={tab}
                onClick={() => handleTabChange(tab)}
                style={{ opacity: isPending ? 0.7 : 1 }}  // покажем, что идёт переход
              >
                {tab}
              </button>
            ))}
          </nav>
    
          {isPending ? <Spinner /> : <TabContent tab={activeTab} />}
        </>
      )
    }

    useDeferredValue: откладываем дорогой рендеринг

    useDeferredValue откладывает обновление значения — похоже на debounce, но умнее:

    import { useDeferredValue } from 'react'
    
    function SearchResults({ query }) {
      const deferredQuery = useDeferredValue(query)
      // deferredQuery обновится позже, когда React найдёт время
    
      // Пока deferredQuery !== query — показываем устаревшие данные с затуханием
      const isStale = deferredQuery !== query
    
      return (
        <div style={{ opacity: isStale ? 0.7 : 1 }}>
          <SlowList query={deferredQuery} />  {/* тяжёлый компонент */}
        </div>
      )
    }
    
    // В родителе — только срочное обновление input:
    function Search() {
      const [query, setQuery] = useState('')
    
      return (
        <>
          <input value={query} onChange={e => setQuery(e.target.value)} />
          <SearchResults query={query} />
        </>
      )
    }

    Автоматическая пакетная обработка (Automatic Batching)

    В React 17 обновления state внутри async-функций (setTimeout, fetch) не группировались:

    // React 17: 2 рендера
    setTimeout(() => {
      setCount(c => c + 1)  // рендер 1
      setFlag(f => !f)      // рендер 2
    }, 1000)
    
    // React 18: автоматически 1 рендер (batching везде!)
    setTimeout(() => {
      setCount(c => c + 1)  //   setFlag(f => !f)      // → один рендер
    }, 1000)

    Это бесплатное улучшение производительности без изменения кода.

    Strict Mode в React 18

    В React 18 Strict Mode намеренно монтирует, размонтирует и снова монтирует компоненты. Это помогает найти побочные эффекты, которые не очищаются правильно в useEffect.

    // В development режиме с Strict Mode:
    // useEffect вызовется дважды — чтобы проверить корректность cleanup
    useEffect(() => {
      const subscription = subscribe()
      return () => subscription.unsubscribe()  // должна работать правильно
    }, [])

    Примеры

    Демонстрация концепции срочных vs отложенных обновлений через requestAnimationFrame и setTimeout, имитация useTransition и useDeferredValue

    // Демонстрируем концепцию "конкурентных" обновлений через JS-примитивы.
    // В React это startTransition/useTransition/useDeferredValue.
    
    // --- Симуляция "тяжёлого" рендера ---
    
    function heavyComputation(query, itemCount = 5000) {
      const start = performance.now()
      const items = []
      // Имитация дорогой фильтрации большого списка
      for (let i = 0; i < itemCount; i++) {
        const item = 'Товар ' + i
        if (item.toLowerCase().includes(query.toLowerCase())) {
          items.push(item)
        }
      }
      const elapsed = Math.round(performance.now() - start)
      return { items: items.slice(0, 10), time: elapsed, total: items.length }
    }
    
    // --- Паттерн 1: Синхронный (React 17 стиль) ---
    // Каждое нажатие клавиши -> немедленный тяжёлый рендер
    
    function syncSearch(query) {
      const start = performance.now()
      const result = heavyComputation(query)
      const uiUpdateTime = Math.round(performance.now() - start)
    
      console.log('Синхронный поиск "' + query + '":')
      console.log('  UI заблокирован на:', uiUpdateTime + 'мс')
      console.log('  Найдено:', result.total, 'результатов')
      console.log('  ⚠️  Пользователь не может нажать кнопки во время поиска!')
      return result
    }
    
    // --- Паттерн 2: startTransition (React 18 стиль) ---
    // Срочное обновление (input) немедленно, тяжёлое (список) — позже
    
    function createTransitionSearch() {
      let pendingQuery = null
      let isPending = false
      let transitionId = 0
    
      function startTransition(callback) {
        isPending = true
        const currentId = ++transitionId
    
        // Переносим выполнение за пределы текущего стека вызовов
        // (в React это делается через планировщик — Scheduler)
        setTimeout(() => {
          if (currentId !== transitionId) {
            console.log('  [Transition] Прерван устаревший переход #' + currentId)
            return
          }
          callback()
          isPending = false
        }, 0)
      }
    
      function handleInput(query) {
        // 1. Срочное: обновить поле ввода НЕМЕДЛЕННО
        pendingQuery = query
        console.log('
    Ввод "' + query + '": поле обновлено мгновенно ✓')
        console.log('isPending:', true, '(идёт отложенное обновление)')
    
        // 2. Несрочное: тяжёлая фильтрация — через transition
        startTransition(() => {
          const result = heavyComputation(query)
          console.log('Transition завершён для "' + query + '":')
          console.log('  Найдено:', result.total, 'за', result.time + 'мс')
          console.log('  isPending:', false)
        })
      }
    
      return { handleInput, getIsPending: () => isPending }
    }
    
    // --- Паттерн 3: useDeferredValue (отложенное значение) ---
    
    function createDeferredValue(delay = 0) {
      let currentValue = ''
      let deferredValue = ''
      let deferredUpdateTimer = null
    
      function update(newValue) {
        currentValue = newValue
        const isStale = currentValue !== deferredValue
    
        console.log('
    Обновление: "' + newValue + '"')
        console.log('  currentValue:', currentValue, '(срочное, обновлено сразу)')
        console.log('  deferredValue:', deferredValue, '(устаревшее, пока не обновлено)')
        console.log('  isStale:', isStale)
    
        if (deferredUpdateTimer) clearTimeout(deferredUpdateTimer)
    
        // Откладываем обновление deferredValue
        deferredUpdateTimer = setTimeout(() => {
          deferredValue = currentValue
          const result = heavyComputation(deferredValue)
          console.log('  deferredValue обновлён: "' + deferredValue + '"')
          console.log('  Список перерендерен, найдено:', result.total)
        }, delay)
      }
    
      return { update }
    }
    
    // --- Демонстрация автоматического batching ---
    
    function demonstrateBatching() {
      console.log('
    === Автоматический Batching ===')
    
      let renderCount = 0
      const state = { count: 0, flag: false, name: '' }
    
      function setState(updates) {
        Object.assign(state, updates)
      }
    
      function scheduleRender() {
        renderCount++
        console.log('Рендер #' + renderCount, '| Состояние:', JSON.stringify(state))
      }
    
      // React 17: setTimeout без batching — 3 рендера
      console.log('React 17 (без batching в async):')
      // Симулируем отдельные рендеры
      setState({ count: state.count + 1 })
      scheduleRender()
      setState({ flag: !state.flag })
      scheduleRender()
      setState({ name: 'Алексей' })
      scheduleRender()
    
      // React 18: автоматический batching — 1 рендер
      console.log('
    React 18 (автоматический batching):')
      renderCount = 0
      // Все обновления сгруппированы в один рендер
      setState({ count: state.count + 1, flag: !state.flag, name: 'Мария' })
      scheduleRender()  // только один рендер!
      console.log('Рендеров:', renderCount, '(вместо 3)')
    }
    
    // --- Запуск демонстраций ---
    
    console.log('=== Синхронный поиск (React 17) ===')
    syncSearch('товар 10')
    
    console.log('
    === Конкурентный поиск (React 18) ===')
    const search = createTransitionSearch()
    search.handleInput('то')
    search.handleInput('тов')
    search.handleInput('товар')  // только этот transition завершится
    
    console.log('
    === useDeferredValue ===')
    const deferred = createDeferredValue(50)
    deferred.update('т')
    deferred.update('то')
    deferred.update('товар 5')  // быстрый ввод
    
    demonstrateBatching()

    Задание

    Реализуй компонент поиска с использованием `useDeferredValue` и `useTransition`. Компонент должен: показывать поле ввода, которое обновляется мгновенно; использовать `useDeferredValue` для отложенного обновления списка результатов; показывать индикатор загрузки когда значения различаются; рендерить список с результатами поиска.

    Подсказка

    useDeferredValue принимает значение: const deferredQuery = React.useDeferredValue(query). isStale = query !== deferredQuery. Для индикатора: {isStale && <span>...</span>}. Для opacity: style={{ opacity: isStale ? 0.6 : 1 }}

    Загружаем среду выполнения...
    Загружаем AI-помощника...