Skip to content

state、props、组件通信与状态管理边界

学 React 到中段,最容易卡住的点通常不是 JSX,而是状态。

真正的问题通常会变成:

  1. 这个数据到底该放哪
  2. 为什么改了数据页面会更新
  3. 为什么不能直接改对象
  4. 两个组件之间到底该怎么通信
  5. 什么时候该用 Context,什么时候还不该上全局状态管理

这篇文章重点讲:

  1. stateprops 的分工
  2. 为什么不能直接修改 state
  3. React 的状态更新和批量更新怎么理解
  4. 组件通信的几种常见方式
  5. 状态应该放在局部、Context 还是 Redux

1. stateprops 的职责分工

这两个词经常一起出现,但职责完全不同。

概念更像什么主要特点
props组件输入由外部传入,组件内部不应该直接改
state组件内部状态由组件自己持有,变化会驱动重新渲染

例如:

jsx
function TodoItem({ title, completed }) {
  return <li>{completed ? "已完成" : "未完成"} - {title}</li>;
}

这里的 titlecompleted 是输入。

而下面这种情况才更适合用 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>;
}

问题在于:

  1. 你直接改了原对象
  2. 新旧引用还是同一个对象
  3. 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 的状态更新怎么理解

状态更新可以抓住两个结论:

  1. 更新不是“立刻同步改 DOM”
  2. 多次更新通常会被合并到一次渲染里处理

例如:

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 会尽量把同一轮里的多次状态更新合并处理。

这样做的主要好处是:

  1. 减少重复渲染
  2. 让更新过程更集中
  3. 给后续调度和优先级管理留下空间

你可以把它理解成:React 不急着每改一次状态就立刻重画页面,而更倾向于收集一轮变更后统一处理。

这也是为什么:

  1. 你不能把 state 当普通变量理解
  2. 读取旧值、闭包旧值、函数式更新会在 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="请输入用户名"
    />
  );
}

这类写法的优点是:

  1. 状态来源清楚
  2. 便于校验
  3. 便于联动展示和提交

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>
    </>
  );
}

它不一定错,只是更适合:

  1. 简单表单
  2. 一次性读取
  3. 和第三方 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 只在一小片组件树共享

优先考虑:

  1. 状态提升
  2. 组合拆分
  3. 必要时配合 Context

7.3 跨页面、跨模块共享,且更新逻辑复杂

这时再考虑 Redux 这类全局状态管理。

🌟 一个很重要的工程判断是,不是状态一多就该上 Redux,而是先判断它到底是不是“全局共享且复杂”。


8. Context 的价值和边界

Context 很适合解决“跨层传参很烦”的问题。

例如:

  1. 主题
  2. 当前用户信息
  3. 权限上下文
  4. 国际化信息

它最适合表达的是,一棵组件子树里都需要感知的上下文。

但它也有边界:

  1. 不负责复杂业务状态治理
  2. 不自动等于高性能
  3. Provider value 频繁变化时,相关消费者也会跟着更新

所以边界要看清:Context 更像共享上下文,不天然等于完整状态管理框架。


9. 什么时候需要 Redux

真正需要 Redux 的场景,通常不是“项目里用的是 React”。

而是这些条件逐渐出现时:

  1. 很多页面共享同一组业务状态
  2. 更新链路复杂
  3. 状态变更需要统一约束
  4. 需要清楚追踪“谁在什么时候改了什么”

如果一个数据只服务于局部页面,放本地 state 往往更简单。


10. 状态层最常见的误区

10.1 一上来就全局状态管理

这会让简单问题也被工程化放大。

10.2 到处把状态提得很高

状态提得过高,会导致很多本来不相关的组件被迫跟着重渲染。

10.3 把 Context 当 Redux 用

Context 可以共享上下文,但不等于它天然适合复杂状态流。

10.4 直接改对象或数组

这样很容易让更新失去可预测性。


11. 一句话总结

React 的状态管理主线,本质上就是要把这些事理顺:数据由谁拥有、从哪里流动、由谁更新,以及这个状态到底该停留在局部、上移到共享父组件、交给 Context,还是进入更重的全局状态管理。

基于 VitePress 构建的个人技术笔记。