Управление состоянием в React: от useState до контекста

Раздел: Frontend -> React разработка

Основные хуки для управления состоянием

Как создать компонент с внутренним состоянием и обновлять его?

Основной инструмент для управления локальным состоянием в функциональных компонентах – хук 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. Результат – отображение текущих значений счётчика и текста.

React JS - comments

En
React js (javascript)