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 позволяет явно разделить обновления на срочные (требуют немедленного отклика) и несрочные (переходы):
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 — хук-версия 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 откладывает обновление значения — похоже на 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} />
</>
)
}В 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)Это бесплатное улучшение производительности без изменения кода.
В 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 — самый значимый релиз за несколько лет. Главное нововведение — конкурентный рендеринг (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 позволяет явно разделить обновления на срочные (требуют немедленного отклика) и несрочные (переходы):
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 — хук-версия 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 откладывает обновление значения — похоже на 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} />
</>
)
}В 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)Это бесплатное улучшение производительности без изменения кода.
В 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 }}