3、状态提升与下放:组件间共享状态、状态提升的原则、状态下放的时机
这一章,我们来聊聊 React 里一个绕不开的话题——状态该放哪。
我刚带团队的时候,经常看到新人把状态满天飞。A 组件需要的数据,放在 B 组件里;B 组件要改的东西,又藏在 C 组件深处。最后调试起来,跟破案似的。说白了,状态管理混乱的根源,往往就是「提升」和「下放」这两个动作没做好。
3.1 组件间共享状态:为什么不能各管各的?
先看一个最简单的场景。两个兄弟组件,一个输入框,一个展示区。输入框改了内容,展示区要同步显示。
你可能会想:各自维护自己的 state 不就行了?
嗯,试试看。
// ❌ 错误示范:各自为政
function InputBox() {
const [text, setText] = useState('');
return <input value={text} onChange={e => setText(e.target.value)} />;
}
function DisplayBox() {
// 这里拿不到 InputBox 里的 text
return <p>当前输入:???</p>;
}
DisplayBox 根本不知道 InputBox 里发生了什么。这就是典型的「状态孤岛」问题。两个组件需要共享同一份数据,但数据被锁在了各自的闭包里。
解决办法?把状态提到它们共同的父组件里去。这就是状态提升。
3.2 状态提升的原则:什么时候该提?提到哪?
我个人习惯用一个简单的判断标准:两个或以上的组件需要读写同一份数据时,这份数据就不该属于其中任何一个。
提到哪?提到它们最近的共同祖先组件里。
核心原则:状态提升的目标是「最近公共祖先」。不是越往上越好,也不是提到全局 store 就万事大吉。
来看一个实际例子。一个搜索页面,有搜索框、搜索结果列表、分页器。这三个组件都需要知道当前的关键词和页码。
// ✅ 正确做法:状态提升到父组件 SearchPage
function SearchPage() {
const [keyword, setKeyword] = useState('');
const [page, setPage] = useState(1);
return (
<div>
<SearchBox keyword={keyword} onSearch={setKeyword} />
<ResultList keyword={keyword} page={page} />
<Pagination page={page} onPageChange={setPage} />
</div>
);
}
这里 keyword 和 page 被提升到了 SearchPage。三个子组件通过 props 读取和修改它们。数据流清晰,谁改了什么一目了然。
我曾经在一个项目中看到有人把 keyword 提到了顶层的 App 组件里。整个应用就一个搜索功能,结果所有组件都跟着重渲染。没必要。提到 SearchPage 就够了。
小技巧:提升状态时,顺便想想「这个状态的生命周期和哪个组件一致」。keyword 只在 SearchPage 范围内有意义,那就放 SearchPage。别往全局 store 里塞。
3.3 状态下放的时机:什么时候该把状态推回去?
状态提升是常态,但有时候也需要「下放」。下放,就是把状态从父组件移回子组件,或者拆成更小的状态单元。
什么时候该下放?我总结了三个信号:
- 父组件因为无关状态频繁重渲染——子组件明明没变,父组件一更新,它也跟着 rerender。
- 状态只被一个子组件使用——那就别放在父组件里占地方。
- 状态变化频率差异大——高频变化的状态和低频变化的状态混在一起,互相拖累。
举个例子。一个表单页面,有用户基本信息(姓名、邮箱)和一个实时搜索的下拉框。搜索下拉框的输入变化非常频繁,但基本信息几乎不变。
// ❌ 状态过度提升:搜索关键词导致整个表单重渲染
function FormPage() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [searchQuery, setSearchQuery] = useState(''); // 这个状态变化太频繁
return (
<div>
<BasicInfo name={name} email={email} />
<SearchDropdown query={searchQuery} onQueryChange={setSearchQuery} />
</div>
);
}
每次 searchQuery 一变,FormPage 重新渲染,BasicInfo 也跟着 rerender。BasicInfo 根本没用到 searchQuery,白干活。
解决办法?把 searchQuery 下放到 SearchDropdown 内部。
// ✅ 状态下放:高频状态隔离到子组件
function SearchDropdown() {
const [query, setQuery] = useState(''); // 自己管自己
// ... 搜索逻辑
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
function FormPage() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
return (
<div>
<BasicInfo name={name} email={email} />
<SearchDropdown /> // 不再传 query 相关 props
</div>
);
}
这样一来,searchQuery 的变化只影响 SearchDropdown 自己。FormPage 和 BasicInfo 完全不受干扰。
注意:下放的前提是「没有其他兄弟组件需要这份状态」。如果 SearchDropdown 的查询结果还要展示在页面的其他地方,那就不能简单下放。这时候可以考虑用 useMemo 或 React.memo 做优化,而不是强行下放。
3.4 提升与下放的动态平衡:一张图看懂
我画了一张流程图,帮你快速判断当前的状态该提升还是该下放。
3.5 实战中的避坑指南
最后,分享几个我踩过的坑。
- 过度提升:把只在一个子组件里用的状态,硬提到父组件甚至全局 store。结果就是「牵一发而动全身」。我曾经见过一个项目,一个按钮的 loading 状态放在 Redux 里,整个页面跟着闪。没必要。
- 过早下放:一开始觉得「这个状态只有 A 组件用」,就放在 A 里。后来 B 组件也需要,又得提回来。来回折腾。我的建议是:先保守提升,等确定只有单个组件使用时再下放。
- 忽略 context 的滥用:很多人觉得「提升太麻烦,直接用 Context 吧」。但 Context 不是银弹。它会让所有消费组件都重渲染。如果你只是两个兄弟组件共享状态,提升到父组件比用 Context 更轻量。
总结一句话:状态提升解决「共享」问题,状态下放解决「性能」问题。两者不是互斥的,而是动态平衡的。每次重构或加新功能时,都问自己一句:「这个状态,放对地方了吗?」