创作者: 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 查看我的更多作品。欢迎大家来观看!