Мы подошли к одному из самых важных, фундаментальных и во всем нашем курсе по React. Если до этого мы в основном занимались статичным отображением данных, то сегодня мы вдохнем в наши компоненты жизнь. Мы научим их реагировать на действия пользователя, запоминать информацию и динамически меняться прямо у нас на глазах. Всё это станет возможным благодаря первому и главному хуку useState.
Введение в состояние (State)
Давайте начнем с простой аналогии. Представьте себе обычный светильник. У него есть кнопка. У светильника есть всего два возможных состояния: «включен» и «выключен». Когда вы нажимаете на кнопку, вы не разбираете светильник и не собираете его заново с новой лампочкой. Вы меняете его внутреннее состояние и он начинает либо светить, либо нет.
Компонент в React очень похож на этот светильник. Пропсы (props) это как данные извне: цвет абажура, длина провода, которые задаются при «сборке» (создании компонента). А состояние (state) это внутренняя, «живая» память компонента. Это данные, которые могут меняться со временем, обычно в ответ на действия пользователя (клики, ввод текста), приход новых данных с сервера или просто по таймеру.
Почему состояние так важно? Потому что React заточен на то, чтобы реагировать на изменения. Когда состояние компонента меняется, React автоматически заново отрисовывает этот компонент (и, при необходимости, его дочерние компоненты), но делает это умно, обновляя только ту часть DOM, которая действительно изменилась. Это ядро его реактивной природы.
До появления хуков в React 16.8 состояние могли использовать только классовые компоненты. Это было одной из причин, по которым классы доминировали. Но с приходом хуков функциональные компоненты получили ту же мощь и даже большую, причем с более простым и лаконичным синтаксисом. И useState это наш главный инструмент для работы с состоянием в функциональных компонентах.
Хук useState: создание состояния
Хук это специальная функция, которая позволяет «подцепиться» к возможностям React. useState это хук, который позволяет нам добавлять состояние в функциональные компоненты.
Давайте посмотрим на его базовый синтаксис. Хук useState возвращает нам массив из двух элементов:
const [state, setState] = useState(initialState);
-
state: Это текущее значение состояния. В первый раз, когда компонент отрисовывается, оно равноinitialState, которое мы передали в хуку. -
setState: Это функция-сеттер. Единственный способ изменить состояние. Когда мы вызываем эту функцию и передаем ей новое значение, React запланирует повторный рендер компонента и при следующем рендереstateбудет уже равно этому новому значению. -
initialState: Начальное значение нашего состояния. Это может быть число, строка, булево значение, массив, объект или дажеnull/undefined.
Синтаксис квадратных скобок [state, setState] это деструктуризация массива. Мы сразу получаем оба значения в отдельные константы. Имена state и setState это просто соглашение. Вы можете называть их как угодно, но обычно называют по схеме: [something, setSomething].
Давайте создадим наш первый компонент с состоянием, тот самый светильник.
import React, { useState } from 'react'; function LightBulb() { // Создаем состояние isOn с начальным значением false (выключено) const [isOn, setIsOn] = useState(false); return ( <div> <p>Лампочка {isOn ? 'включена' : 'выключена'}</p> </div> ); }
Пока что наша лампочка лишь отображает свое состояние, но не может его менять. Она всегда будет выключена. Давайте это исправим.
Обновление состояния с помощью функции-сеттера
Чтобы изменить состояние, мы должны вызвать функцию-сеттер, в нашем случае setIsOn. Мы не можем менять isOn напрямую, например, isOn = true. Это не сработает, потому что React не узнает об изменении и не запустит повторный рендер.
Обычно функцию-сеттер мы вызываем внутри обработчиков событий.
Добавим кнопку, которая будет переключать нашу лампочку:
import React, { useState } from 'react'; function LightBulb() { const [isOn, setIsOn] = useState(false); // Обработчик клика по кнопке const handleClick = () => { // Вызываем сеттер и передаем НОВОЕ значение setIsOn(!isOn); // Устанавливаем противоположное текущему значение }; return ( <div> <p>Лампочка {isOn ? 'включена 🔥' : 'выключена 💡'}</p> {/* Вешаем обработчик на событие onClick */} <button onClick={handleClick}> {isOn ? 'Выключить' : 'Включить'} </button> </div> ); } export default LightBulb;
Теперь у нас есть полностью рабочая лампочка! Что происходит по шагам:
-
Компонент рендерится в первый раз.
isOnравноfalse. -
Пользователь нажимает на кнопку «Включить».
-
Вызывается функция
handleClick. -
Внутри
handleClickмы вызываемsetIsOn(!isOn).!isOnэто!false, то естьtrue. -
React получает команду, что состояние
isOnдолжно поменяться наtrue. Он помечает этот компонент как нуждающийся в повторном рендере. -
React заново вызывает функцию нашего компонента
LightBulb(). -
На этом шаге вызов
useState(false)уже возвращает неfalse, а актуальное значение состоянияtrue. -
Компонент рендерится заново, но теперь
isOnравноtrue. Текст меняется на «Лампочка включена 🔥», а текст на кнопке, на «Выключить».
Это и есть суть React в действии. Мы описываем как компонент должен выглядеть в зависимости от состояния, а React берет на себя всю работу по синхронизации этого описания с реальным DOM.
Функциональное обновление состояния
Иногда новое состояние зависит от предыдущего. В нашем примере с лампочкой мы так и делали: setIsOn(!isOn). Однако, в некоторых ситуациях (особенно при асинхронных операциях) прямое использование значения состояния может привести к ошибкам.
Представьте, что мы дважды быстро вызвали setIsOn(!isOn). React может сгруппировать обновления и в результате получится некорректное значение. Чтобы этого избежать, функция-сеттер может принимать не значение, а функцию обновления. Эта функция получит предыдущее состояние в качестве аргумента и должна вернуть новое состояние.
Перепишем наш обработчик:
const handleClick = () => { // Вместо setIsOn(!isOn) используем функциональную форму setIsOn(prevIsOn => !prevIsOn); };
В этом простом примере разницы не будет, но это правильная и надежная практика, когда новое состояние вычисляется на основе старого. Всегда используйте функциональную форму, если для вычисления нового состояния вам нужно старое.
Правила использования хуков
Хуки не просто функции, они являются частью механизма React. Поэтому чтобы он мог правильно с ними работать, есть два строгих правила:
-
Вызывайте хуки только на верхнем уровне. Не вызывайте хуки внутри циклов, условий или вложенных функций. Все хуки в компоненте должны вызываться в одном и том же порядке при каждом рендере. Именно так React сопоставляет состояния и эффекты между рендерами. Если вы нарушите это правило, например, поместите
useStateвif, состояние может «потеряться» или перемешаться.Неправильно:
if (someCondition) { const [state, setState] = useState(null); // Так делать НЕЛЬЗЯ! }
Правильно:
const [state, setState] = useState(null); // Всегда на верхнем уровне if (someCondition) { // ... используйте state и setState здесь }
-
Вызывайте хуки только из React-функций. Вы можете вызывать хуки из функциональных компонентов или из ваших собственных кастомных хуков. Не вызывайте их из обычных JavaScript-функций.
Следование этим правилам гарантирует, что логика ваших компонентов будет предсказуемой и простой для понимания.
Практические примеры и задачи
Давайте закрепим теорию на более сложных и интересных примерах.
Пример 1: Счетчик
Классический пример, который есть в любой документации.
import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = () => setCount(prevCount => prevCount + 1); const decrement = () => setCount(prevCount => prevCount - 1); const reset = () => setCount(0); return ( <div> <h1>Счетчик: {count}</h1> <button onClick={increment}>+1</button> <button onClick={decrement}>-1</button> <button onClick={reset}>Сбросить</button> </div> ); } export default Counter;
Задача для вас: Добавьте кнопку «+10», которая будет увеличивать значение счетчика сразу на 10. Используйте функциональную форму обновления, чтобы это было безопасно.
Решение
const incrementByTen = () => setCount(prevCount => prevCount + 10); // ... и добавьте кнопку в JSX <button onClick={incrementByTen}>+10</button>
Пример 2: Поле ввода и управляемый компонент
Очень частая задача, это работать с формами. Мы можем заставить состояние React быть «единственным источником истины» для значения в input.
import React, { useState } from 'react'; function InputField() { const [inputValue, setInputValue] = useState(''); const handleInputChange = (event) => { // event.target.value содержит текущее значение input setInputValue(event.target.value); }; const handleSubmit = (event) => { event.preventDefault(); // Предотвращаем перезагрузку страницы alert(`Вы ввели: ${inputValue}`); setInputValue(''); // Очищаем поле после "отправки" }; return ( <form onSubmit={handleSubmit}> <label> Ваше имя: <input type="text" value={inputValue} // Значение input привязано к состоянию onChange={handleInputChange} /> </label> <button type="submit">Отправить</button> <p>Текущий текст: {inputValue}</p> </form> ); } export default InputField;
Это называется управляемый компонент. Значение input полностью управляется состоянием React. Это дает нам полный контроль над ним: мы можем легко его сбрасывать, форматировать или валидировать.
Задача для вас. Создайте компонент для ввода пароля. Добавьте состояние isPasswordVisible (булево) и чекбокс (или кнопку), который будет переключать это состояние. В зависимости от состояния показывайте символы пароля или скрывайте их (меняя атрибут type у input с "password" на "text").
Решение
import React, { useState } from 'react'; function PasswordInput() { const [password, setPassword] = useState(''); const [isPasswordVisible, setIsPasswordVisible] = useState(false); const toggleVisibility = () => { setIsPasswordVisible(prev => !prev); }; return ( <div> <label> Пароль: <input type={isPasswordVisible ? 'text' : 'password'} value={password} onChange={(e) => setPassword(e.target.value)} /> </label> <label> <input type="checkbox" checked={isPasswordVisible} onChange={toggleVisibility} /> Показать пароль </label> </div> ); }
Пример 3: Список дел (основа)
Давайте заложим основу для нашего будущего приложения-трекера задач.
import React, { useState } from 'react'; function TodoList() { const [todos, setTodos] = useState([]); // Начальное состояние - пустой массив const [newTodo, setNewTodo] = useState(''); // Состояние для нового элемента const addTodo = () => { if (newTodo.trim() === '') return; // Не добавляем пустые задачи // Добавляем новую задачу в массив. Используем функциональное обновление. setTodos(prevTodos => [...prevTodos, { id: Date.now(), text: newTodo }]); setNewTodo(''); // Очищаем поле ввода }; const deleteTodo = (id) => { // Удаляем задачу по id. Оставляем все todo, у которых id не равен переданному. setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)); }; return ( <div> <h1>Мой список дел</h1> <div> <input type="text" value={newTodo} onChange={(e) => setNewTodo(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addTodo()} // Добавляем по Enter placeholder="Добавить новую задачу..." /> <button onClick={addTodo}>Добавить</button> </div> <ul> {todos.map(todo => ( <li key={todo.id}> {todo.text} <button onClick={() => deleteTodo(todo.id)}>Удалить</button> </li> ))} </ul> </div> ); } export default TodoList;
Здесь мы используем два состояния: одно для массива задач, другое для поля ввода. Обратите внимание на обновление массива: мы никогда не меняем старый массив (например, через push). Мы всегда создаем новый массив (в данном случае с помощью оператора spread ... и filter). Это одно из ключевых правил React: обновление состояния должно быть иммутабельным (неизменяющим исходные данные).
Задача для вас: Добавьте в каждый элемент списка чекбокс. Создайте новое свойство в объекте задачи, например, completed: false. Добавьте функцию toggleTodo, которая будет менять это свойство на противоположное по клику на чекбокс. Выделите цветом или перечеркните текст выполненных задач.
Решение
import React, { useState } from 'react'; function TodoList() { const [todos, setTodos] = useState([]); const [newTodo, setNewTodo] = useState(''); const addTodo = () => { if (newTodo.trim() === '') return; setTodos(prevTodos => [...prevTodos, { id: Date.now(), text: newTodo, completed: false }]); setNewTodo(''); }; const deleteTodo = (id) => { setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)); }; const toggleTodo = (id) => { setTodos(prevTodos => prevTodos.map(todo => { // Если это не та задача, которую мы ищем, возвращаем ее без изменений if (todo.id !== id) return todo; // Иначе возвращаем копию задачи с измененным свойством completed return { ...todo, completed: !todo.completed }; })); }; return ( <div> {/* ... форма добавления ... */} <ul> {todos.map(todo => ( <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> {todo.text} <button onClick={() => deleteTodo(todo.id)}>Удалить</button> </li> ))} </ul> </div> ); }
Заключение
Вы только что освоили краеугольный камень современного React, хук useState. Теперь ваши компоненты обрели память и способность к взаимодействию. Вы узнали:
-
Что такое состояние и чем оно отличается от пропсов.
-
Как создавать состояние с помощью
useState(initialState). -
Как обновлять состояние с помощью функции-сеттера, в том числе используя функциональное обновление.
-
Каковы главные правила использования хуков.
-
Как на практике применять
useStateдля счетчиков, форм и списков.
Поэкспериментируйте с примерами из этого урока, придумайте свои собственные небольшие компоненты (например, переключатель темы, форма обратной связи, простой чат с сообщениями).
Это был урок 9 из серии «React для начинающих».
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


