Appearance
state、props、组件通信与状态管理边界
学 React 到中段,最容易卡住的点通常不是 JSX,而是状态。
真正的问题通常会变成:
- 这个数据到底该放哪
- 为什么改了数据页面会更新
- 为什么不能直接改对象
- 两个组件之间到底该怎么通信
- 什么时候该用 Context,什么时候还不该上全局状态管理
这篇文章重点讲:
state和props的分工- 为什么不能直接修改 state
- React 的状态更新和批量更新怎么理解
- 组件通信的几种常见方式
- 状态应该放在局部、Context 还是 Redux
1. state 和 props 的职责分工
这两个词经常一起出现,但职责完全不同。
| 概念 | 更像什么 | 主要特点 |
|---|---|---|
props | 组件输入 | 由外部传入,组件内部不应该直接改 |
state | 组件内部状态 | 由组件自己持有,变化会驱动重新渲染 |
例如:
jsx
function TodoItem({ title, completed }) {
return <li>{completed ? "已完成" : "未完成"} - {title}</li>;
}这里的 title、completed 是输入。
而下面这种情况才更适合用 state:
jsx
function SearchBox() {
const [keyword, setKeyword] = useState("");
return <input value={keyword} onChange={e => setKeyword(e.target.value)} />;
}这里的 keyword 是组件自己维护的状态。
🌟 把这句话记住:props 决定组件拿到什么,state 决定组件当前是什么状态。
2. 为什么不能直接修改 state
这是 React 最基础但也最容易被忽略的边界。
例如下面这种写法就有问题:
jsx
function WrongCounter() {
const [state, setState] = useState({ count: 0 });
const handleClick = () => {
state.count += 1;
setState(state);
};
return <button onClick={handleClick}>{state.count}</button>;
}问题在于:
- 你直接改了原对象
- 新旧引用还是同一个对象
- React 的更新判断和优化很容易因此失去稳定前提
更自然的做法是返回一个新对象:
jsx
function CorrectCounter() {
const [state, setState] = useState({ count: 0 });
const handleClick = () => {
setState(prev => ({ ...prev, count: prev.count + 1 }));
};
return <button onClick={handleClick}>{state.count}</button>;
}React 并不是“神奇地禁止你改对象”,而是 它需要一个更稳定、可预测的更新模型。
3. React 的状态更新怎么理解
状态更新可以抓住两个结论:
- 更新不是“立刻同步改 DOM”
- 多次更新通常会被合并到一次渲染里处理
例如:
jsx
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
};
return <button onClick={handleClick}>{count}</button>;
}很多人第一次看到会以为点击一次变成 2。
但实际上这里两次都读到了同一轮渲染里的 count,所以结果通常只会加 1。
如果你真正想基于前一个结果连续更新,更稳的写法是:
jsx
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};🌟 这个例子最重要的结论不是“它背后的细节定义”,而是 React 状态更新更像排队处理,而不是每次调用都立刻生效。
4. 什么叫批量更新
React 会尽量把同一轮里的多次状态更新合并处理。
这样做的主要好处是:
- 减少重复渲染
- 让更新过程更集中
- 给后续调度和优先级管理留下空间
你可以把它理解成:React 不急着每改一次状态就立刻重画页面,而更倾向于收集一轮变更后统一处理。
这也是为什么:
- 你不能把 state 当普通变量理解
- 读取旧值、闭包旧值、函数式更新会在 React 里反复出现
5. 受控组件为什么总和 state 一起讲
因为很多表单输入场景,本质上就是“输入值是否由 React 状态接管”。
5.1 受控组件
受控组件指的是 输入值由 React state 控制。
jsx
function LoginForm() {
const [username, setUsername] = useState("");
return (
<input
value={username}
onChange={e => setUsername(e.target.value)}
placeholder="请输入用户名"
/>
);
}这类写法的优点是:
- 状态来源清楚
- 便于校验
- 便于联动展示和提交
5.2 非受控组件
非受控组件更接近 输入值主要由 DOM 自己保存。
jsx
function SearchInput() {
const inputRef = useRef(null);
const handleSearch = () => {
console.log(inputRef.current.value);
};
return (
<>
<input ref={inputRef} />
<button onClick={handleSearch}>搜索</button>
</>
);
}它不一定错,只是更适合:
- 简单表单
- 一次性读取
- 和第三方 DOM 库交互
6. 组件通信最常见的几种方式
React 里最稳定的通信主线其实不复杂。
6.1 父传子:props
这是最基础也最常见的方式。
适合 上层有数据,下层只负责展示或触发动作。
6.2 子传父:回调函数
React 没有“子组件直接改父组件 state”这种推荐模型。
更常见的做法是父组件把回调传下去:
jsx
function Parent() {
const [count, setCount] = useState(0);
return <Child onIncrease={() => setCount(prev => prev + 1)} />;
}
function Child({ onIncrease }) {
return <button onClick={onIncrease}>+1</button>;
}6.3 兄弟组件通信:状态提升
两个兄弟组件要共享数据时,更自然的做法通常不是彼此直接通信,而是把共享状态提到它们共同的父组件。
这就是 React 里很重要的一条原则,谁拥有状态,谁就掌握更新入口。
6.4 跨层共享:Context
当数据需要跨很多层传递,而且层层手动透传 props 很痛苦时,可以考虑 Context。
例如主题、登录信息、语言配置这类全局上下文。
但 Context 不是“全局状态管理的万能替代品”,后面会专门再展开。
7. 状态应该放在哪里
这是 React 项目里非常核心的判断题。
可以按下面这条线判断:
7.1 只在当前组件里使用
优先放本地 state。
7.2 只在一小片组件树共享
优先考虑:
- 状态提升
- 组合拆分
- 必要时配合 Context
7.3 跨页面、跨模块共享,且更新逻辑复杂
这时再考虑 Redux 这类全局状态管理。
🌟 一个很重要的工程判断是,不是状态一多就该上 Redux,而是先判断它到底是不是“全局共享且复杂”。
8. Context 的价值和边界
Context 很适合解决“跨层传参很烦”的问题。
例如:
- 主题
- 当前用户信息
- 权限上下文
- 国际化信息
它最适合表达的是,一棵组件子树里都需要感知的上下文。
但它也有边界:
- 不负责复杂业务状态治理
- 不自动等于高性能
- Provider value 频繁变化时,相关消费者也会跟着更新
所以边界要看清:Context 更像共享上下文,不天然等于完整状态管理框架。
9. 什么时候需要 Redux
真正需要 Redux 的场景,通常不是“项目里用的是 React”。
而是这些条件逐渐出现时:
- 很多页面共享同一组业务状态
- 更新链路复杂
- 状态变更需要统一约束
- 需要清楚追踪“谁在什么时候改了什么”
如果一个数据只服务于局部页面,放本地 state 往往更简单。
10. 状态层最常见的误区
10.1 一上来就全局状态管理
这会让简单问题也被工程化放大。
10.2 到处把状态提得很高
状态提得过高,会导致很多本来不相关的组件被迫跟着重渲染。
10.3 把 Context 当 Redux 用
Context 可以共享上下文,但不等于它天然适合复杂状态流。
10.4 直接改对象或数组
这样很容易让更新失去可预测性。
11. 一句话总结
React 的状态管理主线,本质上就是要把这些事理顺:数据由谁拥有、从哪里流动、由谁更新,以及这个状态到底该停留在局部、上移到共享父组件、交给 Context,还是进入更重的全局状态管理。