← React/State и useState#260 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

State и useState

Что такое state

State (состояние) — это данные компонента, которые меняются со временем и при изменении вызывают перерендер. Если props — это данные снаружи (от родителя), то state — данные внутри компонента.

Примеры state: счётчик кликов, открыт/закрыт модальный диалог, текст в поле ввода, результат API-запроса.

Хук useState

useState — это хук (hook) React, который добавляет state в функциональный компонент:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)  // начальное значение = 0
  //     ^^^^^  ^^^^^^^^
  //     текущее значение  функция обновления

  return (
    <div>
      <p>Счётчик: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

useState возвращает пару: текущее значение и функцию-сеттер. Деструктуризация массива [state, setState] — стандартный паттерн.

Почему нельзя просто изменить переменную

function BadCounter() {
  let count = 0   // обычная переменная

  return (
    <button onClick={() => {
      count++  // ПРОБЛЕМА: React не знает об этом изменении!
               // Компонент НЕ перерендерится
      console.log(count) // число растёт, но UI не обновляется
    }}>
      {count}
    </button>
  )
}

React перерендеривает компонент только когда вызывается `setState`. Прямая мутация переменных React не отслеживает.

Функциональные обновления: prev => prev + 1

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

// Проблематичный вариант (при частых обновлениях count может быть устаревшим):
setCount(count + 1)

// Безопасный вариант — получаем гарантированно актуальное значение:
setCount(prev => prev + 1)

// Особенно важно в setTimeout или Promise:
function handleMultipleClicks() {
  setCount(prev => prev + 1)  // +1
  setCount(prev => prev + 1)  // +1 от актуального
  setCount(prev => prev + 1)  // итого +3
  // vs:
  // setCount(count + 1)  // все три вызовы читают одно и то же count!
  // setCount(count + 1)  // итого только +1
}

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

State нельзя мутировать — нужно создавать новый объект/массив:

const [user, setUser] = useState({ name: 'Алексей', age: 28 })

// НЕПРАВИЛЬНО — мутация:
user.name = 'Мария'  // React не увидит изменение!
setUser(user)        // тот же объект — нет перерендера

// ПРАВИЛЬНО — новый объект:
setUser({ ...user, name: 'Мария' })  // spread + перезапись поля

// Для массивов:
const [items, setItems] = useState(['a', 'b', 'c'])

// НЕПРАВИЛЬНО:
items.push('d')  // мутация!
setItems(items)  // тот же массив — нет перерендера

// ПРАВИЛЬНО:
setItems([...items, 'd'])           // добавить
setItems(items.filter(i => i !== 'b'))  // удалить
setItems(items.map(i => i === 'a' ? 'A' : i))  // обновить

Несколько state-переменных

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)

  // Каждый вызов useState — отдельная переменная состояния
}

Поднятие состояния (Lifting State Up)

Если двум компонентам нужен доступ к одному state — поднимите его в общего родителя:

// Родитель владеет state и передаёт вниз
function App() {
  const [selected, setSelected] = useState(null)

  return (
    <>
      <ItemList onSelect={setSelected} />
      <ItemDetail item={selected} />
    </>
  )
}

Хуки — это замыкания

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

// НЕЛЬЗЯ — хук в условии:
if (condition) {
  const [val, setVal] = useState(0)  // ошибка!
}

// МОЖНО — только на верхнем уровне компонента:
const [val, setVal] = useState(0)
if (condition) { /* используем val */ }

Примеры

Реализация useState с нуля через замыкания — понимаем как React хранит state между рендерами

// Реализуем useState через замыкание.
// Это поможет понять почему хуки работают именно так.

// ============================================================
// Упрощённая реализация React-хранилища state
// ============================================================

// React хранит state в массиве, индексируя по порядку вызовов
const stateStore = {
  states: [],    // массив всех state-значений
  cursor: 0,     // текущий индекс (сбрасывается при каждом рендере)
}

// Наша реализация useState
function useState(initialValue) {
  const index = stateStore.cursor  // запоминаем индекс для ЭТОГО вызова
  stateStore.cursor++

  // При первом вызове — инициализируем значение
  if (stateStore.states[index] === undefined) {
    stateStore.states[index] = initialValue
  }

  const currentValue = stateStore.states[index]

  // setState — обновляет значение по захваченному индексу
  function setState(newValueOrUpdater) {
    if (typeof newValueOrUpdater === 'function') {
      // Функциональный вариант: получаем текущее значение
      stateStore.states[index] = newValueOrUpdater(stateStore.states[index])
    } else {
      stateStore.states[index] = newValueOrUpdater
    }
    // В реальном React здесь бы произошёл re-render
    console.log(`  [setState] индекс=${index}, новое значение=${stateStore.states[index]}`)
  }

  return [currentValue, setState]
}

// ============================================================
// Симулируем компонент-функцию с несколькими useState
// ============================================================

function Counter() {
  stateStore.cursor = 0  // React сбрасывает cursor перед каждым рендером!

  const [count, setCount] = useState(0)       // index = 0
  const [step, setStep] = useState(1)         // index = 1
  const [history, setHistory] = useState([])  // index = 2

  return { count, step, history, setCount, setStep, setHistory }
}

console.log('=== Первый рендер ===')
let state = Counter()
console.log('count:', state.count)    // 0
console.log('step:', state.step)      // 1
console.log('history:', state.history) // []

console.log('\n=== Обновляем count ===')
state.setCount(prev => prev + state.step)  // функциональный update: 0 + 1 = 1

console.log('\n=== Второй рендер (симуляция) ===')
state = Counter()  // повторный вызов — читаем обновлённые значения
console.log('count:', state.count)    // 1

console.log('\n=== Демонстрация иммутабельности ===')

// Массив state — создаём новый массив, не мутируем
state.setHistory(prev => [...prev, state.count])  // добавить
console.log('\n=== Третий рендер ===')
state = Counter()
console.log('history:', state.history)  // [1]

// Ключевой вывод:
console.log('\nПорядок вызовов useState критически важен!')
console.log('React использует индекс в массиве для связи state с компонентом.')
console.log('Именно поэтому хуки нельзя вызывать в условиях — порядок должен быть стабильным.')

State и useState

Что такое state

State (состояние) — это данные компонента, которые меняются со временем и при изменении вызывают перерендер. Если props — это данные снаружи (от родителя), то state — данные внутри компонента.

Примеры state: счётчик кликов, открыт/закрыт модальный диалог, текст в поле ввода, результат API-запроса.

Хук useState

useState — это хук (hook) React, который добавляет state в функциональный компонент:

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)  // начальное значение = 0
  //     ^^^^^  ^^^^^^^^
  //     текущее значение  функция обновления

  return (
    <div>
      <p>Счётчик: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

useState возвращает пару: текущее значение и функцию-сеттер. Деструктуризация массива [state, setState] — стандартный паттерн.

Почему нельзя просто изменить переменную

function BadCounter() {
  let count = 0   // обычная переменная

  return (
    <button onClick={() => {
      count++  // ПРОБЛЕМА: React не знает об этом изменении!
               // Компонент НЕ перерендерится
      console.log(count) // число растёт, но UI не обновляется
    }}>
      {count}
    </button>
  )
}

React перерендеривает компонент только когда вызывается `setState`. Прямая мутация переменных React не отслеживает.

Функциональные обновления: prev => prev + 1

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

// Проблематичный вариант (при частых обновлениях count может быть устаревшим):
setCount(count + 1)

// Безопасный вариант — получаем гарантированно актуальное значение:
setCount(prev => prev + 1)

// Особенно важно в setTimeout или Promise:
function handleMultipleClicks() {
  setCount(prev => prev + 1)  // +1
  setCount(prev => prev + 1)  // +1 от актуального
  setCount(prev => prev + 1)  // итого +3
  // vs:
  // setCount(count + 1)  // все три вызовы читают одно и то же count!
  // setCount(count + 1)  // итого только +1
}

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

State нельзя мутировать — нужно создавать новый объект/массив:

const [user, setUser] = useState({ name: 'Алексей', age: 28 })

// НЕПРАВИЛЬНО — мутация:
user.name = 'Мария'  // React не увидит изменение!
setUser(user)        // тот же объект — нет перерендера

// ПРАВИЛЬНО — новый объект:
setUser({ ...user, name: 'Мария' })  // spread + перезапись поля

// Для массивов:
const [items, setItems] = useState(['a', 'b', 'c'])

// НЕПРАВИЛЬНО:
items.push('d')  // мутация!
setItems(items)  // тот же массив — нет перерендера

// ПРАВИЛЬНО:
setItems([...items, 'd'])           // добавить
setItems(items.filter(i => i !== 'b'))  // удалить
setItems(items.map(i => i === 'a' ? 'A' : i))  // обновить

Несколько state-переменных

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)

  // Каждый вызов useState — отдельная переменная состояния
}

Поднятие состояния (Lifting State Up)

Если двум компонентам нужен доступ к одному state — поднимите его в общего родителя:

// Родитель владеет state и передаёт вниз
function App() {
  const [selected, setSelected] = useState(null)

  return (
    <>
      <ItemList onSelect={setSelected} />
      <ItemDetail item={selected} />
    </>
  )
}

Хуки — это замыкания

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

// НЕЛЬЗЯ — хук в условии:
if (condition) {
  const [val, setVal] = useState(0)  // ошибка!
}

// МОЖНО — только на верхнем уровне компонента:
const [val, setVal] = useState(0)
if (condition) { /* используем val */ }

Примеры

Реализация useState с нуля через замыкания — понимаем как React хранит state между рендерами

// Реализуем useState через замыкание.
// Это поможет понять почему хуки работают именно так.

// ============================================================
// Упрощённая реализация React-хранилища state
// ============================================================

// React хранит state в массиве, индексируя по порядку вызовов
const stateStore = {
  states: [],    // массив всех state-значений
  cursor: 0,     // текущий индекс (сбрасывается при каждом рендере)
}

// Наша реализация useState
function useState(initialValue) {
  const index = stateStore.cursor  // запоминаем индекс для ЭТОГО вызова
  stateStore.cursor++

  // При первом вызове — инициализируем значение
  if (stateStore.states[index] === undefined) {
    stateStore.states[index] = initialValue
  }

  const currentValue = stateStore.states[index]

  // setState — обновляет значение по захваченному индексу
  function setState(newValueOrUpdater) {
    if (typeof newValueOrUpdater === 'function') {
      // Функциональный вариант: получаем текущее значение
      stateStore.states[index] = newValueOrUpdater(stateStore.states[index])
    } else {
      stateStore.states[index] = newValueOrUpdater
    }
    // В реальном React здесь бы произошёл re-render
    console.log(`  [setState] индекс=${index}, новое значение=${stateStore.states[index]}`)
  }

  return [currentValue, setState]
}

// ============================================================
// Симулируем компонент-функцию с несколькими useState
// ============================================================

function Counter() {
  stateStore.cursor = 0  // React сбрасывает cursor перед каждым рендером!

  const [count, setCount] = useState(0)       // index = 0
  const [step, setStep] = useState(1)         // index = 1
  const [history, setHistory] = useState([])  // index = 2

  return { count, step, history, setCount, setStep, setHistory }
}

console.log('=== Первый рендер ===')
let state = Counter()
console.log('count:', state.count)    // 0
console.log('step:', state.step)      // 1
console.log('history:', state.history) // []

console.log('\n=== Обновляем count ===')
state.setCount(prev => prev + state.step)  // функциональный update: 0 + 1 = 1

console.log('\n=== Второй рендер (симуляция) ===')
state = Counter()  // повторный вызов — читаем обновлённые значения
console.log('count:', state.count)    // 1

console.log('\n=== Демонстрация иммутабельности ===')

// Массив state — создаём новый массив, не мутируем
state.setHistory(prev => [...prev, state.count])  // добавить
console.log('\n=== Третий рендер ===')
state = Counter()
console.log('history:', state.history)  // [1]

// Ключевой вывод:
console.log('\nПорядок вызовов useState критически важен!')
console.log('React использует индекс в массиве для связи state с компонентом.')
console.log('Именно поэтому хуки нельзя вызывать в условиях — порядок должен быть стабильным.')

Задание

Создай компонент App с кнопкой-счётчиком. Используй useState для хранения значения. Кнопка "+ 1" увеличивает счётчик, кнопка "- 1" уменьшает. Отображай текущее значение в теге h2.

Подсказка

useState(0) возвращает [значение, функция-обновления]. Запиши как: const [count, setCount] = useState(0). В JSX переменные вставляются через {фигурные скобки}: {count}. В onClick используй функциональное обновление: c => c - 1 для уменьшения.

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