响应式数据 — useState
useState 是 React 最重要的 Hook。本章你将理解 React 的状态管理机制,并实现分类筛选功能。
React 的「响应式」与 Vue3 有何不同?
Vue3 用 ref/reactive 创建响应式数据,数据变了视图自动更新。
React 的思路不同:当你修改 state,React 会用新的 state 重新执行一遍组件函数,渲染出新的视图。
这就是 React 名字的由来——组件对 state 的变化做出「反应」。
useState 基本用法
useState 是 React 最基础的 Hook,用来给函数组件添加状态。
实例
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 开头的函数是约定俗成的命名规范,不是硬性要求,但全社区都这么写。
为什么不能直接修改 state?
在 React 中,state 是只读的。必须通过 setter 函数更新,不能直接改。
实例
function ArticleManager() {
const [title, setTitle] = useState('默认标题')
// 错误写法:直接修改
// title = '新标题' // React 不知道你改了,不会重新渲染
// 正确写法:通过 setter 更新
function updateTitle() {
setTitle('新标题') // React 知道状态变了,会重新渲染
}
return (
<div>
<p>{title}</p>
<button onClick={updateTitle}>修改标题</button>
</div>
)
}
这背后是 React 的 不可变(Immutable) 原则:不修改旧数据,而是用新数据替换。
更新数组和对象的正确姿势
对于数组和对象,不能直接 push、splice 或修改属性,必须创建新副本。
实例
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React', done: false },
{ id: 2, text: '写博客', done: true }
])
// 添加一项:展开旧数组 + 新元素
function addTodo(text) {
const newTodo = { id: Date.now(), text, done: false }
setTodos([...todos, newTodo]) // 用展开运算符创建新数组
}
// 修改一项: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>
)
}
| 操作 | 错误写法 | 正确写法 |
|---|---|---|
| 添加 | arr.push(item) | setArr([...arr, item]) |
| 修改 | arr[0] = newVal | setArr(arr.map(el => el.id === id ? ... : el)) |
| 删除 | 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 === '全部') 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 布局实战', 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: '一文学会弹性布局', category: 'CSS', date: '2024-05-01' },
])
const [activeCategory, setActiveCategory] = useState('全部')
// 提取所有分类(去重)
const categories = useMemo(() => {
const cats = articles.map(a => a.category)
return ['全部', ...new Set(cats)]
}, [articles])
// 根据分类过滤文章
const filteredArticles = useMemo(() => {
if (activeCategory === '全部') return articles
return articles.filter(a => a.category === activeCategory)
}, [articles, activeCategory])
return (
<div className="app">
<header className="navbar">
<h1 className="logo">RUNOOB Blog</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 RUNOOB Blog. Powered by 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 → 重新渲染 → 新视图」的数据驱动模式。
