Реактивные данные — useState

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


Чем «реактивность» React отличается от Vue3?

Vue3 использует ref/reactive для создания реактивных данных; когда данные изменяются, представление обновляется автоматически.

Подход React отличается:Когда вы изменяете состояние, React повторно выполняет функцию компонента с новым состоянием и рендерит новое представление。

В этом и заключается происхождение названия React — компоненты «реагируют» на изменения состояния.


Базовое использование useState

useStateЭто самый базовый хук React, используемый для добавления состояния в функциональные компоненты.

Пример

// Импорт useState через деструктуризацию из react
import { useState } from 'react'

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

  function handleClick() {
    setCount(count + 1)    // Обновить состояние, React автоматически повторно отрендерит компонент
  }

  return (
    <div>
      <p>Количество кликов:{count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  )
}

Каждый раз при нажатии кнопки,setCountзначение count обновляется, React повторно выполняет функцию Counter с новым значением, возвращает новый JSX, и страница обновляется автоматически.

Соглашение React: имена состояний используют[something, setSomething]деструктуризация. Функции, начинающиеся с set, — это общепринятая практика именования, а не жёсткое требование, но всё сообщество пишет именно так.


Почему мы не можем напрямую изменять состояние?

В React,состояние доступно только для чтения. Его нужно обновлять через функцию-сеттер; нельзя изменять его напрямую.

Пример

import { useState } from 'react'

function ArticleManager() {
  const [title, setTitle] = useState('Default Title')

  // Неправильный способ: прямое изменение
  // title = 'New Title' // React не знает, что вы его изменили, поэтому не перерендерит

  // Правильный способ: обновление через сеттер
  function updateTitle() {
    setTitle('New Title')  // React знает, что состояние изменилось, и перерендерит
  }

  return (
    <div>
      <p>{title}</p>
      <button onClick={updateTitle}>Обновить заголовок</button>
    </div>
  )
}

За этим стоит ReactImmutable-принцип: не изменяйте старые данные, а заменяйте их новыми.


Правильный способ обновления массивов и объектов

Для массивов и объектов нельзя напрямую вызывать push, splice или изменять свойства; нужно создать новую копию.

Пример

import { useState } from 'react'

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Write Blog', done: истина }
  ])

  // Добавить элемент: развернуть старый массив + новый элемент
  function addTodo(text) {
    const newTodo = { id: Date.now(), text, done: false }
    setTodos([...todos, newTodo])        // Использовать оператор spread для создания нового массива
  }

  // Изменить элемент: перебрать с помощью map, найти нужный для изменения, вернуть новый объект
  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ))
  }

  // Удалить элемент: используйте filter, чтобы удалить ненужный
  function removeTodo(id) {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
          {todo.done ? <s>{todo.text}</s> : todo.text}
        </li>
      ))}
    </ul>
  )
}
ДействиеНеправильный способПравильный способ
Addarr.push(item)setArr([...arr, item])
Изменитьarr[0] = newValsetArr(arr.map(el => el.id === id ? ... : el))
Deletearr.splice(i, 1)setArr(arr.filter(el => el.id !== id))
Изменение свойств объектаobj.name = 'xxx'setObj({ ...obj, name: 'xxx' })

Основной принцип: всегда заменяйте старое на новый массив/новый объект. Оператор расширения...и методы массивов, которые вы изучили (map, filter), — лучшие помощники для иммутабельных обновлений.


useMemo — производные вычисляемые значения

useMemoИспользуется для кэширования результатов вычислений. Когда зависимости не изменились, пропускает повторное вычисление, чтобы избежать ненужных затрат производительности.

Пример

import { useState, useMemo } from 'react'

function ArticleFilter() {
  const [articles] = useState([
    { id: 1, title: 'Начало работы с React', category: 'React' },
    { id: 2, title: 'Объяснение Promise', category: 'JavaScript' },
    { id: 3, title: 'Grid-раскладка', category: 'CSS' },
    { id: 4, title: 'React Hooks', category: 'React' }
  ])
  const [activeCategory, setActiveCategory] = useState('Все')

  // useMemo кэширует результат фильтрации; он пересчитывается только при изменении зависимостей
  const filteredArticles = useMemo(() => {
    console.log('Пересчитать результаты фильтрации')  // Проверьте эффект кэширования
    if (activeCategory === 'All') return articles
    return articles.filter(a => a.category === activeCategory)
  }, [articles, activeCategory])  // Массив зависимостей

  // Используйте useMemo для вычисления статистики
  const totalCount = useMemo(() => articles.length, [articles])
  const filteredCount = useMemo(() => filteredArticles.length, [filteredArticles])

  return (
    <div>
      <p>Всего{totalCount}статей, сейчас{filteredCount}статья</p>
      <button onClick={() => setActiveCategory('Все')}>Все</button>
      <button onClick={() => setActiveCategory('React')}>React</button>
      <button onClick={() => setActiveCategory('JavaScript')}>JavaScript</button>
      <button onClick={() => setActiveCategory('CSS')}>CSS</button>

      {filteredArticles.map(a => (
        <div key={a.id}><h3>{a.title}</h3><span>{a.category}</span></div>
      ))}
    </div>
  )
}

Массив зависимостей useMemo(fn, deps) должен быть заполнен полностью: все внутренние переменные компонента, используемые в функции, должны быть объявлены в массиве. React решает, нужно ли пересчитывать, через поверхностное сравнение.


Практика: добавление фильтрации по категориям в блог

Пример

// Путь к файлу: src/App.jsx
import { useState, useMemo } from 'react'
import './App.css'

function App() {
  const [articles] = useState([
    { id: 1, title: 'Полное руководство по началу работы с React', summary: 'Изучаем React с нуля', category: 'React', date: '2024-05-10' },
    { id: 2, title: 'Асинхронное программирование на JS: объяснение', summary: 'Понимание Promise и async/await', category: 'JavaScript', date: '2024-05-08' },
    { id: 3, title: 'CSS Grid Layout в действии', summary: 'Реализация адаптивных макетов с помощью Grid', category: 'CSS', date: '2024-05-05' },
    { id: 4, title: 'React Hooks: глубокое погружение', summary: 'Глубокое понимание useState и useEffect', category: 'React', date: '2024-05-03' },
    { id: 5, title: 'Полное руководство по Flexbox', summary: 'Изучаем Flex Layout в одной статье', category: 'CSS', date: '2024-05-01' },
  ])

  const [activeCategory, setActiveCategory] = useState('Все')

  // Извлечь все категории (удалить дубликаты)
  const categories = useMemo(() => {
    const cats = articles.map(a => a.category)
    return ['All', ...new Set(cats)]
  }, [articles])

  // Фильтровать статьи по категории
  const filteredArticles = useMemo(() => {
    if (activeCategory === 'All') return articles
    return articles.filter(a => a.category === activeCategory)
  }, [articles, activeCategory])

  return (
    <div className="app">
      <header className="navbar">
        <h1 className="logo">ПРИМЕР Блог</h1>
        <nav>
          <a href="/">Главная</a>
          <a href="#">О нас</a>
        </nav>
      </header>

      <main className="container">
        <h2 className="section-title">Последние статьи</h2>

        {/* Группа кнопок фильтра категорий */}
        <div className="category-bar">
          {categories.map(cat => (
            <button
              key={cat}
              className={activeCategory === cat ? 'active' : ''}
              onClick={() => setActiveCategory(cat)}
            >
              {cat}
            </button>
          ))}
        </div>

        <p className="result-info">Всего{filteredArticles.length}статей</p>

        {filteredArticles.length === 0 ? (
          <p className="empty-tip">В этой категории пока нет статей</p>
        ) : (
          <div className="article-grid">
            {filteredArticles.map(article => (
              <div key={article.id} className="article-card">
                <div className="card-content">
                  <span className="card-category">{article.category}</span>
                  <h3>{article.title}</h3>
                  <p>{article.summary}</p>
                  <span className="card-date">{article.date}</span>
                </div>
              </div>
            ))}
          </div>
        )}
      </main>

      <footer className="footer">
        <p>© 2024 Пример блога. Работает на React.</p>
      </footer>
    </div>
  )
}

export default App

Пример

/* Путь к файлу: добавить стили кнопок фильтра категорий в src/App.css */
.category-bar {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.category-bar button {
  padding: 6px 16px;
  border: 1px solid #ddd;
  border-radius: 20px;
  background: #fff;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.2s;
}

.category-bar button.active {
  background: #1976d2;
  color: #fff;
  border-color: #1976d2;
}

.category-bar button:hover {
  border-color: #1976d2;
}

.result-info {
  color: #999;
  font-size: 14px;
  margin-bottom: 16px;
}

Краткое содержание главы

В этой главе вы освоили три ключевые концепции управления состоянием в React: useState для создания и обновления состояния, принцип неизменяемого обновления (оператор расширения + map/filter) и useMemo для кэширования производных вычисляемых значений.

На примере функции фильтрации по категориям вы познакомились с управляемым данными шаблоном React: "setState → повторный рендеринг → новое представление".

Другие расширения