React Front-end Инженер

React Front-end Инженер

Роадмап навыков для прокачки

Как работают useState и useReducer

ReactХукиuseState, useReducer и useContext

Основная идея

useState и useReducer — это хуки для управления локальным состоянием компонента. useState подходит для простых значений, useReducer — для сложной логики обновления состояния с множеством действий.

Ключевые аспекты

  • useState — хранит одно значение и предоставляет функцию для его обновления
  • useReducer — использует reducer-функцию для обработки различных типов действий
  • Иммутабельность — оба хука требуют возврата нового значения, а не мутации существующего
  • Ререндер — изменение состояния вызывает повторный рендер компонента

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

  • useState — для примитивов, простых объектов, независимых значений
  • useReducer — для связанных данных, сложной логики обновления, когда следующее состояние зависит от предыдущего

Принцип работы

  1. При вызове setState или dispatch React планирует ререндер
  2. При следующем рендере хук возвращает новое значение состояния
  3. Компонент рендерится с обновлёнными данными

Частые ошибки на собеседованиях

  • Считают, что setState обновляет состояние синхронно — на самом деле обновление асинхронное
  • Мутируют объект вместо создания нового — React не увидит изменений
  • Не понимают, что useReducer — это «useState на стероидах» с централизованной логикой
  • Забывают, что начальное значение устанавливается только при первом рендере
  • Путают когда использовать функцию-апдейтер: она нужна при зависимости от предыдущего значения

Введение и проблематика

В React компоненты должны «запоминать» данные между рендерами: введённый текст, выбранные элементы, загруженные данные. Обычные переменные не подходят — они сбрасываются при каждом рендере.

useState и useReducer решают эту проблему — они сохраняют данные между рендерами и вызывают обновление интерфейса при изменении.

useState — это фактически упрощённый useReducer. Под капотом они работают похоже, но useState проще в использовании для простых случаев.


Базовая теория

useState — хранение одного значения

tsx
const [value, setValue] = useState(initialValue)

Как это работает:

  1. При первом рендере React создаёт «ячейку памяти» со значением initialValue
  2. value — текущее значение из этой ячейки
  3. setValue — функция для записи нового значения
  4. При вызове setValue React планирует ререндер с новым значением

Code Example 1: Что произойдёт при клике на кнопку? Как useState сохраняет значение между рендерами?

tsx
function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <button onClick={() => setCount(count + 1)}>
      Нажато {count} раз
    </button>
  )
}

useReducer — централизованная логика обновления

tsx
const [state, dispatch] = useReducer(reducer, initialState)

Как это работает:

  1. reducer — функция, которая получает текущее состояние и action, возвращает новое состояние
  2. dispatch — функция для отправки action в reducer
  3. React вызывает reducer(currentState, action) и сохраняет результат

Code Example 2: В чём разница между useState и useReducer в этих примерах? Когда лучше использовать useReducer?

tsx
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
 
  return (
    <>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  )
}

Практические примеры

Простой счётчик

Code Example 3: Какой подход проще для счётчика с тремя кнопками? Какой будет предпочтительнее при добавлении новых действий?

tsx
function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

Форма с несколькими полями

tsx
function Form() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [age, setAge] = useState(0)
 
  // Много отдельных обработчиков
  const handleNameChange = (e) => setName(e.target.value)
  const handleEmailChange = (e) => setEmail(e.target.value)
  const handleAgeChange = (e) => setAge(Number(e.target.value))
 
  const reset = () => {
    setName('')
    setEmail('')
    setAge(0)
  }
 
  return (
    <form>
      <input value={name} onChange={handleNameChange} />
      <input value={email} onChange={handleEmailChange} />
      <input type="number" value={age} onChange={handleAgeChange} />
      <button type="button" onClick={reset}>Сброс</button>
    </form>
  )
}

Ключевые концепции

Иммутабельность

🚫

Никогда не мутируйте state напрямую! React не увидит изменений.

Code Example 4: Почему Version A не работает? Как правильно обновлять объект в state?

tsx
// ❌ Неправильно — мутация
const [user, setUser] = useState({ name: 'John', age: 25 })
user.name = 'Jane'  // Мутация!
setUser(user)       // React не увидит изменений — это тот же объект
 
// ✅ Правильно — новый объект
setUser({ ...user, name: 'Jane' })

Асинхронность обновлений

⚠️

setState не обновляет значение мгновенно. Новое значение доступно только при следующем рендере.

Code Example 5: Что выведет console.log? Почему значение ещё не обновилось?

tsx
const [count, setCount] = useState(0)
 
function handleClick() {
  setCount(count + 1)
  console.log(count) // Всё ещё 0!
  // Новое значение будет доступно при следующем рендере
}

Функция-апдейтер

Когда новое значение зависит от предыдущего, используйте функцию:

Code Example 6: Какой результат будет в каждом случае при быстрых кликах? Почему?

tsx
// ❌ Проблема при быстрых кликах
setCount(count + 1)
setCount(count + 1)
// Результат: +1 вместо +2 (оба вызова видят старый count)
 
// ✅ Решение — функция-апдейтер
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// Результат: +2 (каждый вызов получает актуальное значение)

Когда использовать useState vs useReducer

КритерийuseStateuseReducer
Простые значения✅ ИдеальноИзбыточно
Сложные объектыМожно✅ Лучше
Много связанных действийМного set* функций✅ Один dispatch
Сложная логика обновленияЛогика в компоненте✅ Логика в reducer
ТестированиеСложнее✅ Reducer легко тестировать

Когда выбрать useState

Используйте для:

  • Примитивных значений (числа, строки, boolean)
  • Простых объектов без сложной логики обновления
  • Независимых друг от друга значений

Когда выбрать useReducer

Используйте для:

  • Сложных объектов с множеством полей
  • Когда есть несколько типов действий
  • Когда следующее состояние сложно вычисляется из предыдущего
  • Когда нужно легко тестировать логику обновления

Пограничные кейсы

Батчинг обновлений

React группирует несколько обновлений в один ререндер:

tsx
function handleClick() {
  setCount(c => c + 1)
  setFlag(f => !f)
  setName('New')
  // Только ОДИН ререндер, а не три!
}

Инициализация только при первом рендере

Code Example 7: В чём разница между этими двумя вариантами? Какой более производительный?

tsx
// ❌ Функция вызывается при КАЖДОМ рендере
const [data, setData] = useState(expensiveCalculation())
 
// ✅ Функция вызывается только при ПЕРВОМ рендере
const [data, setData] = useState(() => expensiveCalculation())

Вопросы интервьюера

Q: Почему после setState значение не меняется сразу?

setState только «планирует» обновление. React собирает все обновления и применяет их оптом при следующем рендере. Это называется батчинг.

Q: Можно ли вызвать useState внутри условия?

Нет! Хуки должны вызываться в одном и том же порядке при каждом рендере. React полагается на порядок вызовов для связи состояния с компонентом.

Q: В чём преимущество useReducer для форм?

Вся логика обновления сосредоточена в одной функции reducer. Легко добавлять новые действия, тестировать логику отдельно от компонента, отслеживать историю изменений.

Q: Как правильно обновить вложенный объект?

Создавать новый объект на каждом уровне вложенности: setState(prev => ({ ...prev, nested: { ...prev.nested, field: newValue } })).


Источники

Code Example 1: useState basic counter

❓ Что произойдёт при клике на кнопку? Как useState сохраняет значение между рендерами?

tsx
function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <button onClick={() => setCount(count + 1)}>
      Нажато {count} раз
    </button>
  )
}

Code Example 2: useReducer basic counter

❓ В чём разница между useState и useReducer в этих примерах? Когда лучше использовать useReducer?

tsx
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      return state
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 })
 
  return (
    <>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  )
}

Code Example 3: useState vs useReducer counter comparison

❓ Какой подход проще для счётчика с тремя кнопками? Какой будет предпочтительнее при добавлении новых действий?

useState:

tsx
function Counter() {
  const [count, setCount] = useState(0)
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  )
}

useReducer:

tsx
type Action = { type: 'inc' } | { type: 'dec' } | { type: 'reset' }
 
function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'inc': return state + 1
    case 'dec': return state - 1
    case 'reset': return 0
  }
}
 
function Counter() {
  const [count, dispatch] = useReducer(reducer, 0)
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'inc' })}>+</button>
      <button onClick={() => dispatch({ type: 'dec' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  )
}

Code Example 4: Object mutation problem

❓ Почему Version A не работает? Как правильно обновлять объект в state?

Version A:

tsx
const [user, setUser] = useState({ name: 'John', age: 25 })
user.name = 'Jane'
setUser(user)

Version B:

tsx
setUser({ ...user, name: 'Jane' })

Code Example 5: setState is asynchronous

❓ Что выведет console.log? Почему значение ещё не обновилось?

tsx
const [count, setCount] = useState(0)
 
function handleClick() {
  setCount(count + 1)
  console.log(count)
}

Code Example 6: Functional updater for multiple updates

❓ Какой результат будет в каждом случае при быстрых кликах? Почему?

Version A:

tsx
setCount(count + 1)
setCount(count + 1)

Version B:

tsx
setCount(prev => prev + 1)
setCount(prev => prev + 1)

Code Example 7: Lazy initialization

❓ В чём разница между этими двумя вариантами? Какой более производительный?

tsx
const [data, setData] = useState(expensiveCalculation())
 
const [data, setData] = useState(() => expensiveCalculation())