现在位置: 首页 > React 教程 > 正文

响应式数据 — useState

useState 是 React 最重要的 Hook。本章你将理解 React 的状态管理机制,并实现分类筛选功能。


React 的「响应式」与 Vue3 有何不同?

Vue3 用 ref/reactive 创建响应式数据,数据变了视图自动更新。

React 的思路不同:当你修改 state,React 会用新的 state 重新执行一遍组件函数,渲染出新的视图

这就是 React 名字的由来——组件对 state 的变化做出「反应」。


useState 基本用法

useState 是 React 最基础的 Hook,用来给函数组件添加状态。

实例

// 从 react 中解构导入 useState
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 函数更新,不能直接改。

实例

import { useState } from 'react'

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 或修改属性,必须创建新副本。

实例

import { useState } from 'react'

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] = newValsetArr(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 用于缓存计算结果。依赖没变时跳过重新计算,避免不必要的性能开销。

实例

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 === '全部') 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 布局实战', 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

实例

/* 文件路径: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 → 重新渲染 → 新视图」的数据驱动模式。