第 3 章

Props、State 与数据流设计

第3章:Props、State 与数据流设计

React 的数据模型建立在两个互补的概念上:Props 和 State。混淆它们,或者不理解数据流为何必须是单向的,是初学者最常遇到的架构问题的根源。

本章核心问题:Props 和 State 各自承担什么职责?单向数据流的设计动机是什么? 读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

单向数据流:为什么,而不只是如何

"单向数据流"是 React 的核心约束,但很多人只知道"是什么",不知道"为什么"。

在一个双向数据绑定的系统中,UI 和数据可以互相修改对方。这在小型应用中非常方便,但随着应用规模增大,状态变化的来源变得难以追踪。

React 的单向数据流规定:数据只从父组件流向子组件(通过 props),子组件如果需要影响父组件,必须通过父组件传递下来的回调函数。状态变更只能通过 setState/useState 的更新函数触发,触发后 React 重新渲染受影响的组件树。

这创造了一个可预测的因果链:数据变化 → React 重新渲染 → UI 更新。

Props 的不可变性

Props 对于接收它们的组件而言是只读的,这不仅是约定俗成,而是有深刻原因的。

考虑这样一个场景:父组件传递了一个对象 prop 给子组件,子组件直接修改了这个对象的属性。由于对象是引用类型,父组件持有的对象也被修改了。但父组件对此一无所知——React 没有机会检测到这个变化并重新渲染。

// 错误:直接修改 props
function UserCard({ user }) {
  user.name = 'Modified'; // 永远不要这样做!
  return <div>{user.name}</div>;
}

// 正确:基于 props 创建新的本地状态
function UserCard({ user }) {
  const [editedName, setEditedName] = useState(user.name);
  return (
    <div>
      <input
        value={editedName}
        onChange={e => setEditedName(e.target.value)}
      />
    </div>
  );
}

State 的语义:快照而非可变变量

React 的 state 不是普通的变量。每次 setState 调用都会触发一次重新渲染,渲染时 useState 返回的是新的 state 值,而非旧值的引用。

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // count 在这次渲染中永远是 0
    // 所以这三次调用等价于 setCount(1),不是 setCount(3)
  }

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

如果需要基于前一个状态进行多次更新,应该使用函数式更新

function handleClick() {
  setCount(c => c + 1);
  setCount(c => c + 1);
  setCount(c => c + 1);
  // 正确:count 会增加 3
}

React 18 引入了自动批处理(Automatic Batching),使得所有场景中的多次 setState 调用都会被批量处理为一次重新渲染。

状态提升:共享状态的正确姿势

当两个组件需要共享同一份数据时,正确的做法是将状态提升到它们最近的公共祖先

function TemperatureConverter() {
  const [celsius, setCelsius] = useState('');
  const fahrenheit = celsius !== '' ? (parseFloat(celsius) * 9/5 + 32).toFixed(2) : '';

  return (
    <div>
      <TemperatureInput
        scale="celsius"
        value={celsius}
        onChange={setCelsius}
      />
      <TemperatureInput
        scale="fahrenheit"
        value={fahrenheit}
        onChange={f => setCelsius(((parseFloat(f) - 32) * 5/9).toFixed(2))}
      />
    </div>
  );
}

注意设计决策:单一数据源(只有摄氏度是状态,华氏度是派生值)、状态提升受控组件


Level 2 · 它是怎么运行的(3-5年经验)

组件通信模式

父向子:Props(基础) — 最基本的通信方式。

子向父:回调 Props

function Parent() {
  const [message, setMessage] = useState('');
  return (
    <div>
      <Child onMessage={setMessage} />
      <p>Message from child: {message}</p>
    </div>
  );
}

function Child({ onMessage }) {
  return (
    <button onClick={() => onMessage('Hello from child!')}>
      Send Message
    </button>
  );
}

跨层级:Context API

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Layout />
    </ThemeContext.Provider>
  );
}

function Button({ children }) {
  const { theme } = useContext(ThemeContext);
  return <button className={`btn-${theme}`}>{children}</button>;
}

Context 适合的场景:主题、语言设置、当前登录用户、全局配置等真正"全局"的数据。不应该把 Context 当作避免 prop drilling 的万能解法

事件总线:应该避免的反模式 — 事件总线破坏单向数据流,使数据来源和去向不可追踪,且在 React DevTools 中无法观察。替代方案:状态提升 + 回调 props,或 Context + reducer,或 Zustand/Redux 等状态库。

受控与非受控组件

受控组件:表单元素的值由 React state 控制。 非受控组件:表单元素管理自己的值,React 通过 ref 在需要时读取。

何时选择非受控组件:集成第三方非 React 库、处理文件上传(<input type="file">)、性能极其敏感的大型表单。


Level 3 · 规范怎么定义的(资深)

React 19 的 useFormStatus 和 useActionState

React 19 为表单引入了专属 Hook,配合 Server Actions 使用:

import { useActionState } from 'react';

async function submitAction(prevState, formData) {
  const name = formData.get('name');
  if (!name) return { error: '姓名不能为空' };
  await saveUser({ name });
  return { success: true };
}

function UserForm() {
  const [state, action, isPending] = useActionState(submitAction, null);

  return (
    <form action={action}>
      <input name="name" />
      {state?.error && <p className="error">{state.error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </button>
    </form>
  );
}

这种模式将表单提交逻辑与渲染逻辑分离,是 React 19 推荐的表单处理方向。

状态设计原则

1. 最小化状态:只将真正需要触发重渲染的数据放入 state,派生数据通过计算得到。

2. 避免状态镜像 props:不要将 props 复制到 state(除非明确需要可编辑的本地副本)。

3. 将相关状态组合在一起:如果两个状态总是一起变化,将它们合并。


Level 4 · 边界与陷阱(所有人)

陷阱一:派生状态存储在 state 中

// 错误:存储派生数据
const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0); // 多余!

// 正确:派生计算
const [items, setItems] = useState([]);
const itemCount = items.length;

每多一个冗余的 state,就多一个需要手动同步的数据源,也多一个 bug 来源。

陷阱二:Props 对象突变导致 React.memo 失效

当父组件传递对象 prop 给使用了 React.memo 的子组件时,如果每次渲染都创建新对象(即使内容相同),memo 的浅比较会判定 props 变化,子组件仍会重渲染。

陷阱三:状态提升导致的 prop drilling 过深

当 prop drilling 超过 3-4 层时,代码维护成本急剧上升。这时应该考虑 Context(低频变化的数据)或外部状态管理库(高频变化的数据),而不是继续向下传递 props。理解 Props 和 State 的边界,以及数据流的方向,是 React 架构设计的基础。

本章评分
4.8  / 5  (83 评分)

💬 留言讨论