React 从入门到生产(二):状态与事件处理

创作者: Yardon | GitHub: github.com/YardonYan | 版本: v1.0


为什么需要状态

上一章我们学会了用 JSX 描述静态 UI------就像拍了张照片,画面定住了,无法改变。

但真实的网页是活的:你点击按钮,数字加一;你在输入框打字,文字实时显示;你切换标签,内容跟着变化。

React 用一个词来形容这种"会变的东西":状态(State)

状态 = 组件记忆的数据,改变时自动触发重新渲染

如果不理解状态,就等于不理解 React 的一半。React 的全名里就有"React(反应)"------它是对状态变化做出反应的库。

打个比方:普通 HTML 页面就像一个公告栏------你把纸贴上去,它就一直在那里,除非你亲手撕掉换新的。React 组件则像一台自动售货机------你投币(改变状态),机器自动推出一罐饮料(更新页面)。


useState:React 的「记忆细胞」

useState 是 React 里最常用的 Hook,没有之一。它的作用就是给函数组件加上「记忆能力」。

jsx 复制代码
import { useState } from 'react';

function Counter() {
  // [当前值, 修改函数] = useState(初始值)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </button>
  );
}

拆解这行代码

javascript 复制代码
const [count, setCount] = useState(0);
//      ↑        ↑              ↑
//    状态变量   更新函数      初始值

这句话翻译成人话就是:

"帮我记住一个叫 count 的值,初始值是 0。以后我要改它的时候,就用 setCount。改完你自己重新渲染页面,别让我操心。"

useState 的几条规则

规则 1:不能在条件或循环里调用

jsx 复制代码
// ❌ 绝对不要!
if (condition) {
  const [state, setState] = useState(0);  // Hook 调用顺序乱了
}

// ✅ 总是在函数顶层调用
const [state, setState] = useState(0);
if (condition) {
  // 使用 state...
}

这背后是 React 的 Hook 实现机制:React 靠调用顺序来追踪每个 useState。如果在条件里调用,不同渲染周期里 Hook 的数量可能不一样,React 就乱了。

想象你在食堂排队打饭。你每次都点三菜一汤,师傅已经记住了你的习惯。有一天你突然说"今天只要两个菜",师傅的手已经习惯性地多打了一个------节奏全乱了。useState 也一样,顺序必须稳定。

规则 2:初始值只在第一次渲染时有效

javascript 复制代码
const [count, setCount] = useState(0);  // 0 只在第一次有用

后续重新渲染时,React 会忽略初始值,直接使用当前存储的值。这一点和直觉可能不同------你可能会以为每次渲染都会重新初始化为 0,但实际上不会。


状态更新是异步的

这是 React 初学者最容易踩的坑。

jsx 复制代码
function BrokenCounter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    console.log(count);  // 打印的是旧值!
  }

  return <button onClick={handleClick}>{count}</button>;
}

点击按钮后,console.log 打印的还是旧值。为什么?因为 setCount 不是"立刻修改",而是"安排一次修改"------React 把你放进排队队列里,等它有空了再统一处理。

解决方案:用函数式更新

javascript 复制代码
function handleClick() {
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  setCount(prev => prev + 1);
  // 连续调三次,count 最终 +3
}

传函数给 setState 时,React 会把前一次更新后的值传进来。这样即使多次调用,每次都能拿到最新的值。

经验法则:如果你的新状态依赖旧状态,用函数式更新。


事件处理:让用户和页面对话

React 的事件处理和对原生 DOM 很像,但有细微差别:

命名:驼峰式

jsx 复制代码
// DOM:   onclick="handleClick()"
// React: onClick={handleClick}

<button onClick={handleClick}>点击</button>
<input onChange={handleChange} />
<form onSubmit={handleSubmit}>

事件对象:SyntheticEvent

React 对原生事件做了一层封装,叫做 SyntheticEvent。大多数情况下,你用起来和原生事件完全一样:

jsx 复制代码
function handleClick(e) {
  e.preventDefault();           // 阻止默认行为(如表单提交)
  e.stopPropagation();          // 阻止冒泡
  console.log(e.target.value);   // 获取触发元素的值
}

传参给事件处理器

jsx 复制代码
function ItemList({ items }) {
  function handleDelete(id) {
    console.log('删除:', id);
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          {/* 用箭头函数传参 */}
          <button onClick={() => handleDelete(item.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

表单受控组件

React 里处理表单的方式比较特别------React 推荐你成为表单数据的"唯一真相来源"。

jsx 复制代码
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    console.log('登录:', { email, password });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="邮箱地址"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="密码"
      />
      <button type="submit">登录</button>
    </form>
  );
}

注意关键词:value={email}onChange。输入框的值完全由 React 状态控制------不是浏览器里用户输入的那个值,而是 React 状态里存的那个值。

受控组件 = 表单的值由 React state 决定,而非浏览器 DOM。

这听起来多了一步,但带来的好处巨大:你可以随时读取表单的值、实时验证、格式化输入(比如自动加空格格式化银行卡号)------这些在传统方式里要费老大的劲。

非受控组件(偶尔用)

有时候你确实不需要实时追踪表单值------比如一个简单的文件上传:

jsx 复制代码
function FileUploader() {
  const fileRef = useRef(null);

  function handleSubmit(e) {
    e.preventDefault();
    console.log(fileRef.current.files[0]);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" ref={fileRef} />
      <button type="submit">上传</button>
    </form>
  );
}

这种情况下,你不需要每次用户选文件都更新 state,只要在提交时读一次就够了。useRef 就是干这个的------它像一个可以在渲染之间持久存在的盒子,但修改它不会触发重新渲染。


状态提升:兄弟组件通信

假如你有两个兄弟组件 A 和 B,它们需要共享同一个状态。React 的解决方案叫「状态提升」------把状态放到它们的共同父组件里。

复制代码
        Parent(count 在这里)
       /      \
      A        B
  (读 count)(修改 count)
jsx 复制代码
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <ChildA count={count} />
      <ChildB onIncrement={() => setCount(count + 1)} />
    </div>
  );
}

function ChildA({ count }) {
  return <p>当前计数:{count}</p>;
}

function ChildB({ onIncrement }) {
  return <button onClick={onIncrement}>+1</button>;
}

状态提升是 React 里最基本的跨组件通信模式。等你掌握了它,再去学 Context 和状态管理库,就会发现它们不过是这个模式在更大规模上的变体。


常见陷阱与最佳实践

陷阱 1:用 state 存可以计算出来的值

jsx 复制代码
// ❌ 冗余状态
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');  // 不需要!

// ✅ fullName 可以直接计算
const fullName = `${firstName} ${lastName}`;

当多个状态之间有确定的数学关系时,不要让它们各自独立------减少状态数量就是减少 bug 数量。

陷阱 2:直接修改对象或数组

jsx 复制代码
// ❌ React 检测不到变化(引用不变)
const [user, setUser] = useState({ name: 'Yardon', age: 25 });
user.age = 26;             // 直接改了
setUser(user);             // 但引用没变,React 以为没更新

// ✅ 创建新对象(引用变了)
setUser({ ...user, age: 26 });

// ✅ 数组同理
const [list, setList] = useState([1, 2, 3]);
setList([...list, 4]);              // 添加
setList(list.filter(n => n !== 2));  // 删除

React 用 Object.is() 来判断状态是否变化(类似 === 比较)。直接修改对象或数组不会改变引用地址,React 就把这次更新跳过了。


实战:计数器与待办事项

把今天学的融合在一起,写一个简单的待办事项应用:

jsx 复制代码
function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  function addTodo() {
    if (!input.trim()) return;
    setTodos([...todos, { id: Date.now(), text: input, done: false }]);
    setInput('');  // 清空输入框
  }

  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  }

  function removeTodo(id) {
    setTodos(todos.filter(todo => todo.id !== id));
  }

  return (
    <div className="todo-app">
      <h2>📝 待办事项</h2>
      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && addTodo()}
          placeholder="添加新事项..."
        />
        <button onClick={addTodo}>添加</button>
      </div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>✕</button>
          </li>
        ))}
      </ul>
      <p>共 {todos.length} 项,已完成 {todos.filter(t => t.done).length} 项</p>
    </div>
  );
}

这个不到 50 行的组件里,你已经用到了:

  • useState 管理数组和字符串
  • 不可变更新(展开运算符创建新数组/新对象)
  • 受控组件(input 的 value 绑定到 state)
  • 事件处理(onClick、onChange、onKeyDown)

本章小结

概念 一句话总结
useState 让函数组件拥有记忆,值变即重新渲染
异步更新 setState 是"预约"而非"执行",依赖旧值用函数式更新
受控组件 input/select/textarea 的值由 React state 驱动
不可变更新 对象和数组要创建新引用,不能用 mutation
状态提升 兄弟组件通信:状态上移到公共父组件

学到这儿,你已经可以写出有交互的 React 应用了。但交互只触发了一次性的 UI 变化------下一章我们要讨论 useEffect,让组件能跟"外部世界"(API 调用、定时器、浏览器事件)打交道。


📌 创作者: Yardon | 🏠 个人网站: GlimmerAI.top

📖 本章是「React 从入门到生产 」系列的第 2 章。上一章:JSX 与组件思维 | 下一章:副作用与数据获取

🌟 如果你觉得有帮助,欢迎访问 GlimmerAI.top 查看我的更多作品。欢迎大家来观看!

相关推荐
条俐开水喉1 小时前
高密度AI算力服务器机房U位动态调度管理方案
运维·服务器·人工智能
fl1768311 小时前
密封圈质量检测密封圈缺陷检测数据集VOC+YOLO格式1295张5类别有增强
人工智能·yolo·机器学习
花间相见1 小时前
【语音识别】— FunASR 项目详解与 Fun-ASR-Nano 实战
人工智能·语音识别
南屹川1 小时前
【网络安全】Web安全防护:从XSS到CSRF的攻防实战
人工智能
2501_945837431 小时前
AI 自动化领域的变革性价值
人工智能
Mr -老鬼1 小时前
EasyClick AI全自动编程,AI IDE选型真难?
ide·人工智能·自动化·ai编程·easyclick·易点云测
Maimai108081 小时前
React 项目目录结构怎么设计:从基础分层到真实业务落地
前端·javascript·react.js·microsoft·前端框架
comcoo1 小时前
OpenClaw AI 聊天网关配置教程|Gateway 启动与完整使用指南
运维·人工智能·elasticsearch·gateway·openclaw安装包·open claw部署
Csvn1 小时前
CSS 技巧:移动端适配
前端