03-状态管理与路由——01. useState + Props - 状态提升
01. useState + Props - 状态提升
一、5W1H 概述
| 维度 | 内容 |
|---|---|
| What | 将多个组件共享的状态提升到最近的共同父组件中 |
| Why | 实现兄弟组件间的数据共享和同步 |
| When | 多个组件需要共享同一状态、兄弟组件通信 |
| Where | 共同父组件中定义状态,通过 props 传递给子组件 |
| Who | 需要实现简单状态共享的开发者 |
| How | 父组件使用 useState,通过 props 传递状态和更新函数 |
二、What - 什么是状态提升?
状态提升(Lifting State Up)是 React 中的一种模式,当多个组件需要共享同一状态时,将该状态提升到它们的共同父组件中,然后通过 props 传递给子组件。
// ❌ 状态在各自组件中,无法共享 function ComponentA() { const [data, setData] = useState(''); } function ComponentB() { const [data, setData] = useState(''); } // ✅ 状态提升到父组件 function Parent() { const [data, setData] = useState(''); return ( <> <ComponentA data={data} setData={setData} /> <ComponentB data={data} setData={setData} /> </> ); }三、Why - 为什么需要状态提升?
3.1 解决兄弟组件通信问题
在 React 中,数据是单向流动的。兄弟组件之间无法直接共享状态,需要通过父组件中转。
3.2 保持数据一致性
将状态提升后,所有子组件都使用同一份数据,确保数据同步。
3.3 简化状态管理
对于简单的父子/兄弟通信,状态提升是最直接、最简单的方案。
四、When - 何时使用状态提升?
| 场景 | 是否使用 | 说明 |
|---|---|---|
| 兄弟组件需要共享状态 | ✅ 推荐 | 最直接的解决方案 |
| 父子组件需要共享状态 | ✅ 推荐 | 原生 props 传递 |
| 跨多层组件共享状态 | ❌ 不推荐 | 会导致 props drilling |
| 全局状态 | ❌ 不推荐 | 使用 Context 或状态管理库 |
五、Where - 在哪里使用?
- 共同父组件中定义状态
- 子组件中通过 props 接收状态和更新函数
// 父组件 function Parent() { const [sharedState, setSharedState] = useState(initialValue); return <Child state={sharedState} setState={setSharedState} />; } // 子组件 function Child({ state, setState }) { return <button onClick={() => setState(newValue)}>{state}</button>; }六、Who - 谁需要使用?
所有需要实现组件间状态共享的 React 开发者。
七、How - 如何使用状态提升?
7.1 基础示例:兄弟组件通信
// 父组件 function Parent() { const [sharedData, setSharedData] = useState(''); return ( <div> <ChildA sharedData={sharedData} setSharedData={setSharedData} /> <ChildB sharedData={sharedData} /> </div> ); } // 子组件 A:可以修改状态 function ChildA({ sharedData, setSharedData }) { return ( <div> <input value={sharedData} onChange={(e) => setSharedData(e.target.value)} placeholder="输入内容..." /> </div> ); } // 子组件 B:只读取状态 function ChildB({ sharedData }) { return <div>接收到的数据: {sharedData}</div>; }7.2 计数器示例
function CounterParent() { const [count, setCount] = useState(0); return ( <div> <CounterDisplay count={count} /> <CounterControls setCount={setCount} /> </div> ); } function CounterDisplay({ count }) { return <h1>当前计数: {count}</h1>; } function CounterControls({ setCount }) { return ( <div> <button onClick={() => setCount(prev => prev + 1)}>+1</button> <button onClick={() => setCount(prev => prev - 1)}>-1</button> <button onClick={() => setCount(0)}>重置</button> </div> ); }7.3 表单示例
function FormParent() { const [formData, setFormData] = useState({ username: '', email: '', age: '' }); const handleChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); }; return ( <div> <FormInput label="用户名" value={formData.username} onChange={(value) => handleChange('username', value)} /> <FormInput label="邮箱" value={formData.email} onChange={(value) => handleChange('email', value)} /> <FormInput label="年龄" value={formData.age} onChange={(value) => handleChange('age', value)} /> <FormPreview data={formData} /> </div> ); } function FormInput({ label, value, onChange }) { return ( <div> <label>{label}:</label> <input value={value} onChange={(e) => onChange(e.target.value)} /> </div> ); } function FormPreview({ data }) { return ( <div> <h3>预览</h3> <p>用户名: {data.username}</p> <p>邮箱: {data.email}</p> <p>年龄: {data.age}</p> </div> ); }7.4 Todo 列表示例
function TodoApp() { const [todos, setTodos] = useState([ { id: 1, text: '学习 React', completed: false }, { id: 2, text: '学习状态提升', completed: false } ]); const addTodo = (text) => { setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]); }; const toggleTodo = (id) => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }; const deleteTodo = (id) => { setTodos(prev => prev.filter(todo => todo.id !== id)); }; return ( <div> <TodoInput onAdd={addTodo} /> <TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} /> <TodoStats todos={todos} /> </div> ); } function TodoInput({ onAdd }) { const [text, setText] = useState(''); const handleSubmit = (e) => { e.preventDefault(); if (text.trim()) { onAdd(text); setText(''); } }; return ( <form onSubmit={handleSubmit}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button type="submit">添加</button> </form> ); } function TodoList({ todos, onToggle, onDelete }) { return ( <ul> {todos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} /> <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}> {todo.text} </span> <button onClick={() => onDelete(todo.id)}>删除</button> </li> ))} </ul> ); } function TodoStats({ todos }) { const total = todos.length; const completed = todos.filter(t => t.completed).length; const active = total - completed; return ( <div> <p>总计: {total} | 已完成: {completed} | 未完成: {active}</p> </div> ); }7.5 购物车示例
function CartApp() { const [cart, setCart] = useState([]); const addToCart = (product) => { setCart(prev => { const existing = prev.find(item => item.id === product.id); if (existing) { return prev.map(item => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } return [...prev, { ...product, quantity: 1 }]; }); }; const updateQuantity = (id, quantity) => { setCart(prev => prev.map(item => item.id === id ? { ...item, quantity: Math.max(0, quantity) } : item ).filter(item => item.quantity > 0) ); }; const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0); return ( <div> <ProductList onAddToCart={addToCart} /> <Cart cart={cart} onUpdateQuantity={updateQuantity} /> <CartTotal totalPrice={totalPrice} /> </div> ); } function ProductList({ onAddToCart }) { const products = [ { id: 1, name: '商品 A', price: 100 }, { id: 2, name: '商品 B', price: 200 }, { id: 3, name: '商品 C', price: 150 } ]; return ( <div> {products.map(product => ( <div key={product.id}> <span>{product.name} - ¥{product.price}</span> <button onClick={() => onAddToCart(product)}>加入购物车</button> </div> ))} </div> ); } function Cart({ cart, onUpdateQuantity }) { if (cart.length === 0) { return <p>购物车是空的</p>; } return ( <ul> {cart.map(item => ( <li key={item.id}> {item.name} - ¥{item.price} x {item.quantity} <button onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}>+</button> <button onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}>-</button> </li> ))} </ul> ); } function CartTotal({ totalPrice }) { return <h3>总计: ¥{totalPrice}</h3>; }八、状态提升 vs Props Drilling
// ❌ 不好的做法:过度提升导致 props drilling function App() { const [user, setUser] = useState(null); return ( <Header user={user} /> <Main user={user} setUser={setUser} /> <Footer user={user} /> ); } // ✅ 好的做法:只提升必要的状态 function App() { return ( <Header /> <UserSection /> {/* 用户状态只在这里使用 */} <Footer /> ); } function UserSection() { const [user, setUser] = useState(null); return <Profile user={user} setUser={setUser} />; }九、常见陷阱
9.1 过度提升
// ❌ 将不需要共享的状态也提升了 function Parent() { const [count, setCount] = useState(0); const [text, setText] = useState(''); // 只在 ChildA 中使用 return ( <> <ChildA count={count} setCount={setCount} text={text} setText={setText} /> <ChildB count={count} /> </> ); } // ✅ 只提升共享的状态 function Parent() { const [count, setCount] = useState(0); return ( <> <ChildA count={count} setCount={setCount} /> <ChildB count={count} /> </> ); }9.2 忘记使用函数式更新
// ❌ 可能导致过期状态问题 function Parent() { const [count, setCount] = useState(0); const incrementTwice = () => { setCount(count + 1); setCount(count + 1); // 只增加 1 }; return <Child incrementTwice={incrementTwice} />; } // ✅ 使用函数式更新 function Parent() { const [count, setCount] = useState(0); const incrementTwice = () => { setCount(prev => prev + 1); setCount(prev => prev + 1); // 增加 2 }; return <Child incrementTwice={incrementTwice} />; }十、练习题
基础题
- 实现一个温度转换器:摄氏度和华氏度互相转换
- 实现一个简单的计算器:两个数字输入,显示计算结果
进阶题
- 实现一个可编辑的表格:支持添加、删除、编辑行
十一、小结
| 要点 | 说明 |
|---|---|
| 适用场景 | 兄弟组件通信、简单状态共享 |
| 实现方式 | 父组件 useState + props 传递 |
| 优点 | 简单直接,无需额外依赖 |
| 缺点 | 深层传递会导致 props drilling |
