Props、State 与数据流设计
第3章:Props、State 与数据流设计
React 的数据模型建立在两个互补的概念上:Props 和 State。混淆它们,或者不理解数据流为何必须是单向的,是初学者最常遇到的架构问题的根源。
本章核心问题:Props 和 State 各自承担什么职责?单向数据流的设计动机是什么? 读完本章你将理解:
- 单向数据流的"为什么"而不仅是"如何"
- Props 不可变性的深层原因及其对渲染模型的保证
- 状态提升、组件通信模式,以及受控与非受控组件的选择
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 架构设计的基础。