其中哪些是 state 呢?标记出那些不是的:
- 随着时间推移 保持不变?如此,便不是 state。
- 通过 props 从父组件传递?如此,便不是 state。
- 是否可以基于已存在于组件中的 state 或者 props 进行计算?如此,它肯定不是state!
在 React 中有两种"模型"数据:props 和 state。下面是它们的不同之处:
- props像是传递的参数 。它们使父组件可以传递数据给子组件,定制它们的展示。举个例子,
Form可以传递colorprop 至Button。 - state像是组件的内存。它使组件可以对一些信息保持追踪,并根据交互来改变。举个例子,
Button可以保持对isHoveredstate 的追踪。
一、先建立核心认知:state 和 props 的本质定义
在拆解底层前,先明确两者的本质,这是理解一切的基础:
- props(Properties) :组件的 "外部输入",是父组件传递给子组件的只读数据(可以是基本类型、对象、函数),本质是组件间通信的 "桥梁"。
- state(State) :组件的 "内部状态",是组件自身管理的可变数据,触发更新时会驱动组件重新渲染,本质是组件内部的 "动态数据源"。
二、底层原理:React 如何处理 state 和 props
1. props 的底层机制
props 的核心是单向数据流,其底层逻辑可以拆解为 3 个关键点:
- 只读性:React 设计上强制 props 只读(Immutable),组件内部直接修改 props 会报错。这是为了保证数据流向可追溯(父 → 子),避免 "数据溯源混乱"。
- 浅对比(Shallow Compare) :当父组件重新渲染时,React 会对新旧 props 做浅对比 (只比较引用 / 基本类型值,不深查对象内部):
- 如果 props 没变化,子组件会被 React 优化(跳过重渲染);
- 如果 props 变化(比如引用变了),子组件会触发重渲染。
- 传递链路:props 只能沿组件树 "向下传递"(父 → 子 → 孙),无法反向传递(子 → 父),若需反向通信,需通过 "父传子函数" 实现(子调用父的函数,修改父的 state)。
2. state 的底层机制
state 是 React 组件 "响应式" 的核心,其底层逻辑围绕状态更新队列 和重渲染机制展开:
- 状态更新的异步性 :
setState(类组件)/setXxx(useState)并非立即更新状态,而是将更新请求加入 React 的更新队列,等当前事件循环结束后,React 会批量处理更新队列,再触发组件重渲染。
tsx
// 示例:异步更新的表现
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 输出旧值,因为 setCount 是异步的
};
return <button onClick={handleClick}>点击 {count}</button>;
}
- 状态的不可变更新(Immutable Update) :React 要求更新 state 时不能直接修改原数据(比如直接改 state 里的对象 / 数组),必须返回新的引用,否则 React 无法检测到状态变化,会跳过重渲染:
tsx
const [user, setUser] = useState({ name: "张三" });
// ❌ 错误:直接修改原对象,React 检测不到变化
const handleChange = () => {
user.name = "李四"; // 直接修改原对象
setUser(user); // 引用没变,组件不会重渲染
};
// ✅ 正确:返回新对象,React 检测到引用变化
const handleChange = () => {
setUser({ ...user, name: "李四" }); // 解构生成新对象
};
- 重渲染触发条件 :只有通过 React 提供的更新函数(
setState/setXxx)修改 state,才会触发组件重渲染;直接修改 state 变量(比如count = 1)不会触发重渲染,因为 React 无法监听普通变量的变化。
3. state 和 props 的底层关联
- 当父组件的 state 变化触发重渲染时,会重新计算子组件的 props,若 props 变化则子组件重渲染;
- 子组件的 state 变化仅影响自身及子组件,不会反向影响父组件的 props(单向数据流);
- React 组件的渲染本质是:
UI = f(props, state)(UI 是 props 和 state 的纯函数),props 和 state 是驱动 UI 变化的唯一数据源。
三、核心区别(全维度对比)
| 维度 | state (状态) | props (属性) |
|---|---|---|
| 所有权 | 组件自身拥有(内部生成) | 父组件 / 外部传入(组件无所有权) |
| 可修改性 | 可通过 setState /useState 更新(间接修改) |
只读(组件内部不可改,只能由外部更新) |
| 数据类型 | 任意类型(但推荐不可变类型:对象 / 数组用新引用) | 任意类型(基本类型、对象、函数、JSX 元素) |
| 触发重渲染 | 调用更新函数会触发自身及子组件重渲染 | 父组件传递的 props 变化会触发自身重渲染 |
| 作用域 | 仅当前组件实例有效(隔离) | 可跨组件传递(父 → 子) |
| 初始化方式 | 组件内部初始化(useState(初始值) ) |
父组件传递(无默认值则为 undefined) |
| 默认值处理 | 初始化时直接指定 | 可通过 defaultProps 或解构赋值指定 |
四、典型使用场景(附实战示例)
1. 只用 state 的场景(组件私有动态数据)
适用于组件自己管理、无需和其他组件共享的状态:
-
输入框的实时输入内容
-
弹窗的显示 / 隐藏状态
-
单选框 / 复选框的选中状态
-
独立计数器的数值
tsx
// 示例:弹窗的显示/隐藏(私有 state)
function Modal() {
// 私有状态:控制弹窗显示
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>打开弹窗</button>
{isOpen && (
<div className="modal">
<p>这是弹窗内容</p>
<button onClick={() => setIsOpen(false)}>关闭</button>
</div>
)}
</div>
);
}
2. 只用 props 的场景(纯展示 / 接收外部配置)
适用于组件仅展示数据、无内部逻辑,所有数据 / 行为由外部传入:
-
通用按钮组件(接收文字、点击事件)
-
列表项组件(接收展示数据)
-
卡片组件(接收标题、内容、样式配置)
tsx
// 示例:通用按钮组件(纯 props 接收)
function Button({ text, size = "middle", onClick }) {
// 无 state,仅使用 props 渲染
const sizeClass = `btn-${size}`;
return (
<button className={sizeClass} onClick={onClick}>
{text}
</button>
);
}
// 使用:父组件传递 props
function Parent() {
return <Button text="提交" size="large" onClick={() => alert("提交成功")} />;
}
3. state + props 结合的场景(跨组件共享状态)
这是最常见的场景:父组件用 state 管理共享状态,通过 props 传递给子组件,子组件通过 props 接收的函数修改父组件的 state。
典型场景:共享计数器、表单提交、列表筛选等。
tsx
// 示例:共享计数器(父组件 state + 子组件 props)
import { useState } from "react";
// 父组件:管理共享 state
function Parent() {
const [total, setTotal] = useState(0);
// 定义修改 state 的函数,通过 props 传给子组件
const increment = () => setTotal(total + 1);
const decrement = () => setTotal(total - 1);
return (
<div>
<h2>总次数:{total}</h2>
{/* 传递数据和函数给子组件 */}
<CounterButton text="加1" onClick={increment} />
<CounterButton text="减1" onClick={decrement} />
</div>
);
}
// 子组件:仅使用 props,无自己的 state
function CounterButton({ text, onClick }) {
return <button onClick={onClick}>{text}</button>;
}
4. 易错场景 & 避坑指南
- 坑 1:子组件想修改 props
- ❌ 错误:
props.count = 1(直接修改 props); - ✅ 正确:父组件传递更新函数,子组件调用函数修改父组件的 state。
- ❌ 错误:
- 坑 2:直接修改 state 里的对象 / 数组
- ❌ 错误:
state.list.push(1)(直接改原数组); - ✅ 正确:
setList([...state.list, 1])(生成新数组)。
- ❌ 错误:
- 坑 3:依赖异步 state 更新
- ❌ 错误:连续调用
setCount(count + 1)多次,只生效一次; - ✅ 正确:使用函数式更新
setCount(prev => prev + 1)。
- ❌ 错误:连续调用
五、进阶扩展:state 管理的演进(超出基础但实用)
当项目规模变大,单纯的组件内 state 无法满足需求时,会用到更进阶的状态管理方案,本质是对 state 作用域的扩展:
- useContext + useState:跨多层组件共享 state(避免 props 层层传递,即 "props 钻取");
- Redux/MobX/Zustand:全局状态管理(适用于跨页面、跨组件的全局数据,比如用户信息、购物车);
- React Query/SWR:异步状态管理(专门处理 API 请求的加载、缓存、更新)。
这些方案的核心依然是遵循 React 的单向数据流,只是把 state 的作用域从 "单个组件" 扩展到 "全局 / 跨组件"。
总结
- 底层核心:props 是只读的单向数据流(父→子),state 是组件内部的可变状态,更新需遵循不可变原则,且更新是异步的;
- 使用原则:私有动态数据用 state,外部传入 / 展示数据用 props,跨组件共享状态需提升 state 或用进阶方案;
- 避坑关键:不直接修改 props/state 原数据,依赖 state 最新值时用函数式更新,遵循单向数据流。
记住核心公式 UI = f(props, state),就能理解:React 组件的所有动态变化,本质都是 props 和 state 的变化驱动 UI 重新渲染。