Управление состоянием в React: от useState до контекста
Основные хуки для управления состоянием
Как создать компонент с внутренним состоянием и обновлять его?
Основной инструмент для управления локальным состоянием в функциональных компонентах – хук useState. Он возвращает массив из двух элементов: текущее значение и функцию для его обновления. Пример простого счетчика:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}React js (react js)
При каждом нажатии кнопки вызывается setCount, что запускает повторный рендер компонента с новым значением count. Это базовый пример, который лежит в основе более сложных сценариев.
Типичные ошибки:
- Попытка напрямую мутировать состояние (например, count = 5). Это не вызывает рендер.
- Использование старого значения при вызове setCount без функциональной формы в асинхронных сценариях. Правильно: setCount(prev => prev + 1).
Как работать с более сложной логикой состояния (несколько полей, объекты)?
Если состояние представляет собой объект, обновлять его нужно через копирование. Использование useState остаётся удобным, но требуется ручное слияние. Например, форма с двумя полями:
const [form, setForm] = useState({ name: '', email: '' });
function handleChange(e) {
setForm({ ...form, [e.target.name]: e.target.value });
}При большом количестве полей или вложенности удобнее перейти на useReducer, который предоставляет централизованную логику обновления.
Как вынести сложную логику обновления состояния в отдельную функцию?
Хук useReducer идеален для случаев, когда следующее состояние зависит от предыдущего и требуется несколько сценариев (действий). Пример – управление корзиной покупок:
const initialState = { items: [], total: 0 };
function reducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, items: [...state.items, action.payload], total: state.total + action.payload.price };
case 'remove':
const newItems = state.items.filter(item => item.id !== action.id);
const removed = state.items.find(item => item.id === action.id);
return { ...state, items: newItems, total: state.total - (removed ? removed.price : 0) };
default:
return state;
}
}
function Cart() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Товаров: {state.items.length}, сумма: {state.total}</p>
<button onClick={() => dispatch({ type: 'add', payload: { id: 1, name: 'Книга', price: 500 } })}>Добавить книгу</button>
</div>
);
}Такой подход делает логику предсказуемой и легко тестируемой.
Как передать состояние глубоко в дерево компонентов без пропс-дриллинга?
Для этого используется Context API с хуком useContext. Создается контекст, провайдер оборачивает нужную часть приложения, а дочерние компоненты читают значение через useContext.
const ThemeContext = React.createContext('light');
function App() {
return (
);
}
function Toolbar() {
const theme = useContext(ThemeContext);
return Панель инструментов;
}Это упрощает передачу глобальных настроек, но не заменяет полноценное управление состоянием для частых обновлений (из-за ререндера всех потребителей).
Расширенные примеры работы с состоянием
Ниже представлены сценарии, которые часто встречаются в реальных проектах, с пояснением каждого шага.
Асинхронная загрузка данных с использованием useEffect и useState
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
if (!cancelled) {
setUsers(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err.message);
setLoading(false);
}
});
return () => { cancelled = true; };
}, []);
if (loading) return Загрузка...
;
if (error) return Ошибка: {error}
;
return (
{users.map(user => - {user.name}
)}
);
}Результат выполнения (после загрузки) – список имён пользователей. Флаг cancelled предотвращает обновление состояния после размонтирования компонента, что является частой ошибкой, ведущей к утечкам памяти.
[1] Загрузка...
[2] • Leanne Graham
• Ervin Howell
• ...Кастомный хук для работы с формой
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
setValues({ ...values, [e.target.name]: e.target.value });
};
const reset = () => setValues(initialValues);
return [values, handleChange, reset];
}
// Использование в компоненте
function LoginForm() {
const [credentials, handleChange, reset] = useForm({ username: '', password: '' });
const handleSubmit = (e) => {
e.preventDefault();
console.log('Отправка:', credentials);
reset();
};
return (
);
}Кастомный хук инкапсулирует логику управления формой и может быть переиспользован в разных компонентах. Результат – консольный вывод объекта credentials после отправки формы.
Оптимизация рендеров с помощью useMemo и useCallback
import React, { useState, useMemo, useCallback } from 'react';
function ExpensiveList({ items, onItemClick }) {
const sortedItems = useMemo(() => {
console.log('Сортировка...');
return [...items].sort((a, b) => a - b);
}, [items]);
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
{sortedItems.map(item => (
- handleClick(item)}>{item}
))}
);
}useMemo предотвращает повторную сортировку, если items не изменились. useCallback стабилизирует ссылку на функцию, что помогает избежать лишних рендеров дочерних компонентов, обёрнутых в React.memo. Результат – в консоли сообщение 'Сортировка...' появляется только при изменении items.
Работа с множественными редукторами через combineReducers (имитация Redux)
import React, { useReducer, createContext, useContext } from 'react';
function combineReducers(reducers) {
return (state, action) => {
const newState = {};
for (let key in reducers) {
newState[key] = reducers[key](state[key], action);
}
return newState;
};
}
const countReducer = (state = 0, action) => {
switch (action.type) {
case 'inc': return state + 1;
case 'dec': return state - 1;
default: return state;
}
};
const textReducer = (state = '', action) => {
switch (action.type) {
case 'set': return action.payload;
default: return state;
}
};
const rootReducer = combineReducers({ count: countReducer, text: textReducer });
const StoreContext = createContext();
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(rootReducer, { count: 0, text: '' });
return (
{children}
);
}
function Display() {
const { state } = useContext(StoreContext);
return Счётчик: {state.count}, текст: {state.text}
;
}
function Controls() {
const { dispatch } = useContext(StoreContext);
return (
<>
>
);
}Этот пример показывает, как можно организовать глобальное состояние без внешних библиотек, используя только встроенные средства React. Результат – отображение текущих значений счётчика и текста.