Props 和 State
React 组件通过两种方式处理数据:Props(属性)和 State(状态)。理解它们的区别和使用场景,是掌握 React 的第一步。
Props:组件的配置
Props 是从父组件传递给子组件的数据。可以把 Props 想象成函数的参数——父组件调用子组件时传入的"配置项"。
Props 的核心特点
只读性:子组件不应该修改接收到的 props。这条规则让组件的行为可预测,便于调试。
function Welcome({ name, color = "#2563eb" }) { return ( <div style={{ padding: '10px', borderLeft: `5px solid ${color}`, marginBottom: '10px' }}> <h3 style={{ color: color, margin: 0 }}>你好,{name}!</h3> <p style={{ fontSize: '14px', color: '#666' }}>欢迎来到 React 世界</p> </div> ); } function App() { return ( <div> <Welcome name="张三" color="#10b981" /> <Welcome name="李四" color="#f59e0b" /> <Welcome name="王五" /> </div> ); }
上面的例子展示了 props 的几个用法:
- 解构赋值从 props 对象中提取值
- 默认值语法
color = "#2563eb" - 在 JSX 中使用 props 渲染动态内容
Props 传递模式
子组件向父组件传递数据:通过回调函数
function Child({ onMessage }) { return ( <button onClick={() => onMessage("来自子组件的问候!")}> 向父组件发送消息 </button> ); } function Parent() { const [msg, setMsg] = useState("等待消息..."); return ( <div> <p style={{ color: '#666' }}>{msg}</p> <Child onMessage={(text) => setMsg(text)} /> </div> ); }
这种"回调 props"模式是 React 中子组件与父组件通信的标准方式。
State:组件的记忆
State 是组件内部私有的数据,用于存储随时间变化的值。可以把 State 想象成组件的"记忆"——记录组件在某个时刻的状态。
useState Hook
function Counter() { const [count, setCount] = useState(0); return ( <div style={{ textAlign: 'center', padding: '20px', border: '1px dashed #ccc' }}> <h2 style={{ fontSize: '48px', margin: '10px 0' }}>{count}</h2> <div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}> <button onClick={() => setCount(count - 1)}>-1</button> <button onClick={() => setCount(count + 1)}>+1</button> </div> </div> ); }
State 更新的重要特性
异步更新:setState 不会立即改变 state,而是安排一次重新渲染。
// 错误:连续调用无法累加
function WrongExample() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 基于旧的 count
setCount(count + 1); // 还是基于旧的 count
console.log(count); // 打印旧值
};
return <button onClick={handleClick}>点击</button>;
}
// 正确:使用函数式更新
function RightExample() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1); // 基于最新值
setCount(prev => prev + 1); // 基于最新值
// 最终 count 会增加 2
};
return <button onClick={handleClick}>点击</button>;
}
不可变性:不要直接修改 state 对象,而是创建新对象。
// 错误:直接修改
const [user, setUser] = useState({ name: '张三', age: 20 });
user.age = 21; // 不会触发重新渲染!
setUser(user);
// 正确:创建新对象
setUser({ ...user, age: 21 });
// 或使用 immer 库简化
import { produce } from 'immer';
setUser(produce(draft => { draft.age = 21; }));
Props vs State:核心对比
| 特性 | Props | State |
|---|---|---|
| 来源 | 父组件传入 | 组件内部定义 |
| 可修改性 | 只读(组件内不可改) | 可通过 setter 函数修改 |
| 作用 | 组件的"配置参数" | 组件的"内部记忆" |
| 触发更新 | 父组件传入新值时 | 调用 setter 函数时 |
| 初始值来源 | 父组件决定 | 组件自己决定 |
一个有用的思考方式:Props 像函数参数,State 像组件内部的变量。
状态结构设计
好的状态结构能让组件更易维护、更少出bug。React 官方给出了几个指导原则。
避免冗余状态
不要存储可以从其他状态推导出的值。
// 不好的做法:存储了冗余的 fullName
function BadForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // 冗余!
// 需要同步更新三个状态,容易出错
}
// 好的做法:fullName 在渲染时计算
function GoodForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName; // 计算得出
return <p>欢迎,{fullName}</p>;
}
避免状态重复
当同一份数据在多个地方存储时,更新时容易遗漏某处。
// 不好的做法:items 和 selectedItem 都存储完整对象
const [items, setItems] = useState([...]);
const [selectedItem, setSelectedItem] = useState(items[0]);
// 如果更新 items 中的某项,selectedItem 不会自动更新!
// 好的做法:selectedItem 只存储 ID
const [items, setItems] = useState([...]);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find(item => item.id === selectedId);
合理拆分或合并状态
根据状态的更新频率和逻辑关联性来决定。
// 相关状态可以合并
const [position, setPosition] = useState({ x: 0, y: 0 });
// 而不是
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// 不相关的状态保持独立
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
状态提升
当多个组件需要共享同一个状态时,将状态移动到它们最近的共同父组件中。
经典示例:同步输入
function InputDisplay({ text, onTextChange }) { return ( <input value={text} onChange={(e) => onTextChange(e.target.value)} style={{ width: '100%', marginBottom: '10px', padding: '8px' }} placeholder="输入内容会同步到下方..." /> ); } function Mirror({ text }) { return ( <div style={{ background: '#f3f4f6', padding: '10px', borderRadius: '4px' }}> 镜像文本: <strong>{text}</strong> </div> ); } function Parent() { const [text, setText] = useState(""); return ( <div> <InputDisplay text={text} onTextChange={setText} /> <Mirror text={text} /> </div> ); }
状态提升的核心思想:单一数据源。数据只存在于一个地方,其他组件通过 props 获取,通过回调更新。
状态提升 vs 双向绑定
Vue 等框架提供双向绑定,但 React 选择单向数据流:
数据流向:父 → 子(Props)
事件流向:子 → 父(Callbacks)
这种模式虽然需要更多代码,但数据流向清晰,便于调试和理解。
复杂状态管理:useReducer
当状态更新逻辑复杂时,useState 会变得难以维护。useReducer 提供了一种更结构化的方式。
useState vs useReducer
// 使用 useState:分散的更新逻辑
function TodoListUseState() {
const [todos, setTodos] = useState([]);
const handleAdd = (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};
const handleToggle = (id) => {
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};
const handleDelete = (id) => {
setTodos(todos.filter(t => t.id !== id));
};
// ...
}
// 使用 useReducer:集中的更新逻辑
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'toggle':
return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
case 'delete':
return state.filter(t => t.id !== action.id);
default:
throw new Error('Unknown action');
}
}
function TodoListUseReducer() {
const [todos, dispatch] = useReducer(todosReducer, []);
// 事件处理器变得简洁
const handleAdd = (text) => dispatch({ type: 'add', text });
const handleToggle = (id) => dispatch({ type: 'toggle', id });
const handleDelete = (id) => dispatch({ type: 'delete', id });
}
useReducer 适用场景
- 状态逻辑复杂,有多个子值
- 下一个状态依赖于之前的状态
- 需要触发深层更新的复杂逻辑
- 想要更易于测试的状态逻辑
跨组件状态共享:Context API
当状态需要在深层嵌套的组件间共享时,逐层传递 props 会很繁琐(称为"prop drilling")。Context API 解决了这个问题。
创建和使用 Context
// 1. 创建 Context
const ThemeContext = createContext('light');
// 2. 在父组件提供值
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<DeepChild />
</ThemeContext.Provider>
);
}
// 3. 在深层子组件消费值
function DeepChild() {
const theme = useContext(ThemeContext);
return <div className={theme}>当前主题: {theme}</div>;
}
Context + useReducer:全局状态管理
// 创建 Context const CounterContext = createContext(); // Reducer 函数 function counterReducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: return state; } } // Provider 组件 function CounterProvider({ children }) { const [state, dispatch] = useReducer(counterReducer, { count: 0 }); return ( <CounterContext.Provider value={{ state, dispatch }}> {children} </CounterContext.Provider> ); } // 子组件 A:显示计数 function CounterDisplay() { const { state } = useContext(CounterContext); return <h3 style={{ margin: '10px 0' }}>计数: {state.count}</h3>; } // 子组件 B:控制按钮 function CounterControls() { const { dispatch } = useContext(CounterContext); return ( <div style={{ display: 'flex', gap: '10px' }}> <button onClick={() => dispatch({ type: 'decrement' })}>-1</button> <button onClick={() => dispatch({ type: 'reset' })}>重置</button> <button onClick={() => dispatch({ type: 'increment' })}>+1</button> </div> ); } // 组合使用 function App() { return ( <CounterProvider> <div style={{ textAlign: 'center', padding: '20px' }}> <CounterDisplay /> <CounterControls /> </div> </CounterProvider> ); }
这种模式是 Zustand、Redux 等状态管理库的基础思想。
状态管理最佳实践
1. 状态就近原则
状态应该放在使用它的组件树的尽可能高的位置,但要足够低以覆盖所有需要它的组件。
App
├── Header # 不需要用户状态
├── Main
│ ├── Sidebar # 不需要用户状态
│ └── Content
│ ├── UserList # 需要用户状态
│ └── UserDetail # 需要用户状态
└── Footer # 不需要用户状态
# 用户状态应该放在 Content 组件,而不是 App
2. 区分 UI 状态和业务数据
// UI 状态(本地管理)
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState('info');
// 业务数据(可能需要共享或持久化)
const [user, setUser] = useState(null);
const [products, setProducts] = useState([]);
3. 何时使用外部状态管理库
对于大多数应用,React 内置的状态管理已经足够。考虑使用 Zustand、Jotai 或 Redux 的情况:
- 状态需要在完全不相关的组件间共享
- 状态需要持久化到 localStorage
- 需要复杂的状态更新逻辑
- 需要时间旅行调试
小结
- Props 是父组件传入的配置,只读不可修改
- State 是组件内部的状态,可以通过 setter 函数更新
- 状态提升用于兄弟组件间的状态共享
- useReducer 适合复杂的状态更新逻辑
- Context API 解决深层组件的状态传递问题
- 单向数据流是 React 状态管理的核心理念
练习
- 创建一个
Toggle组件,点击按钮切换"开"和"关"状态 - 实现一个
TemperatureConverter,在摄氏度和华氏度之间双向同步(状态提升练习) - 构建一个显示用户信息的卡片,Props 传入用户信息,State 控制卡片的展开/收起
- 使用 useReducer 重写一个简单的购物车逻辑(添加、删除、修改数量)
- 使用 Context 实现一个主题切换功能(明/暗模式)