Реактивные данные — useState
useState — самый важный хук в React. В этой главе вы разберётесь с механизмом управления состоянием React и реализуете фильтрацию по категориям.
Чем «реактивность» React отличается от Vue3?
Vue3 использует ref/reactive для создания реактивных данных; когда данные изменяются, представление обновляется автоматически.
Подход React отличается:Когда вы изменяете состояние, React повторно выполняет функцию компонента с новым состоянием и рендерит новое представление。
В этом и заключается происхождение названия React — компоненты «реагируют» на изменения состояния.
Базовое использование useState
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,состояние доступно только для чтения. Его нужно обновлять через функцию-сеттер; нельзя изменять его напрямую.
Пример
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 или изменять свойства; нужно создать новую копию.
Пример
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>
)
}
| Действие | Неправильный способ | Правильный способ |
|---|---|---|
| Add | arr.push(item) | setArr([...arr, item]) |
| Изменить | arr[0] = newVal | setArr(arr.map(el => el.id === id ? ... : el)) |
| Delete | arr.splice(i, 1) | setArr(arr.filter(el => el.id !== id)) |
| Изменение свойств объекта | obj.name = 'xxx' | setObj({ ...obj, name: 'xxx' }) |
Основной принцип: всегда заменяйте старое на новый массив/новый объект. Оператор расширения
...и методы массивов, которые вы изучили (map, filter), — лучшие помощники для иммутабельных обновлений.
useMemo — производные вычисляемые значения
useMemoИспользуется для кэширования результатов вычислений. Когда зависимости не изменились, пропускает повторное вычисление, чтобы избежать ненужных затрат производительности.
Пример
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 решает, нужно ли пересчитывать, через поверхностное сравнение.
Практика: добавление фильтрации по категориям в блог
Пример
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
Пример
.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 → повторный рендеринг → новое представление".
Другие расширения