State (состояние) — это данные компонента, которые меняются со временем и при изменении вызывают перерендер. Если props — это данные снаружи (от родителя), то state — данные внутри компонента.
Примеры state: счётчик кликов, открыт/закрыт модальный диалог, текст в поле ввода, результат API-запроса.
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 не отслеживает.
Когда новое состояние зависит от предыдущего, используйте функциональный вариант:
// Проблематичный вариант (при частых обновлениях 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 нельзя мутировать — нужно создавать новый объект/массив:
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)) // обновитьfunction LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
// Каждый вызов useState — отдельная переменная состояния
}Если двум компонентам нужен доступ к одному 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 (состояние) — это данные компонента, которые меняются со временем и при изменении вызывают перерендер. Если props — это данные снаружи (от родителя), то state — данные внутри компонента.
Примеры state: счётчик кликов, открыт/закрыт модальный диалог, текст в поле ввода, результат API-запроса.
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 не отслеживает.
Когда новое состояние зависит от предыдущего, используйте функциональный вариант:
// Проблематичный вариант (при частых обновлениях 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 нельзя мутировать — нужно создавать новый объект/массив:
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)) // обновитьfunction LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
// Каждый вызов useState — отдельная переменная состояния
}Если двум компонентам нужен доступ к одному 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 для уменьшения.