Menu

React 状态提升:在组件之间共享 state

状态提升是指把 state 从两个组件中移出,放到它们最近的共同父组件里,再由父组件通过 props 把值和修改函数传下去。学习什么时候该提升、如何分三步完成,以及什么时候应该把 state 留在组件内部。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

状态提升是指把 state 从需要共享它的组件中移出,放到它们最近的共同父组件里。父组件保存这个值,通过 prop 传下去,同时传一个供子组件调用来修改它的函数,这样所有子组件都根据同一份 state 来渲染。

点击任意面板上的“Show”:它会展开,原来展开的那个会收起。同一时间只能展开一个面板,因为这件事不由面板自己决定。App 持有 openIndex,每个 Panel 只接收 isOpen 和一个 onOpen 函数。

问题:本该保持一致的 state

从最直观的写法开始:每个面板都有自己的 isOpen state。每个面板都能正常工作,但面板之间互不知情,所以没有什么能阻止两个面板同时展开。

把两个面板都展开:它们会同时保持展开。React 中的 state 是声明它的组件私有的,兄弟组件既读不到,也重置不了。当两个组件需要保持一致时,state 就必须放在它们两者之上。

分三步提升 state

把第二个例子改成第一个,需要改三处。

  1. 从子组件中移除 state。 删掉 Panel 里的 useState,改为从 props 读取 isOpen。子组件不再决定自己是否展开。
  2. 由父组件传入值和修改它的方式。 Panel 接收 isOpen 和一个 onOpen 回调。按钮被点击时,子组件调用 onOpen();它并不知道父组件会拿这个做什么。
  3. 把 state 加到共同父组件中。 App 声明 openIndex,再把它转换成每个面板的 props:isOpen={openIndex === 1} 和 onOpen={() => setOpenIndex(1)}。

最近的共同父组件,是渲染了所有需要这份 state 的组件的最低一层组件。这里就是 App。如果这些面板放在一个 Faq 组件里,state 就应该放在 Faq 中,而不是更高的地方。

提升之后,Panel 就由父组件控制,和受控输入框是一个意思:它显示 props 给出的内容,并通过回调报告变化。受控与非受控组件一页介绍了表单元素中的同一思路。

单一数据源

当屏幕上的两个地方显示同一个事实时,只存一次这个事实,其他内容都从它计算出来。温度转换器是最典型的例子:摄氏度和华氏度两个输入框必须始终一致,所以它们不能各自保存一个数字。

在任意一个输入框里输入,另一个都会跟着变化。state 只是一个事实:用户最后输入的数字,以及它用的是哪种温标。另一个输入框的值在渲染时根据它计算出来,所以你正在输入的框始终保持你输入的原样。把初始 state 改成 { value: '212', scale: 'f' },输入框下方的提示就会变成“Water boils.”。

如果把摄氏度和华氏度分别存成两份 state,每个处理函数都得同时更新两者,一旦某个处理函数忘了,两个输入框就对不上了。只存一个值,它就不可能和自己不一致。

把 setter 传下去

子组件只能通过父组件给它的函数来修改父组件的 state。你可以直接传 setter(onSelect={setColor}),也可以传一个做更多事情的函数(onOpen={() => setOpenIndex(1)})。给这个 prop 起一个事件风格的名字,比如 onSelect 或 onChange,而不是 setSelected,这样子组件就不知道父组件如何存储这个值,父组件以后要改也不必动子组件。

ColorPicker 和 Preview 从不直接通信。选择器把选择报告给上层,App 存下来,新的值再向下流到两个组件。

什么时候不该提升 state

提升是有代价的。当 state 放在父组件中时,每次变化都会让父组件重新渲染,默认情况下它的所有子组件也会重新渲染,包括那些根本不用这份 state 的组件。只提升到最近的共同父组件为止,只有一个组件用到的 state 就留在那个组件里。

在输入框里打字,同时观察控制台:每次按键只有 SearchBox 重新渲染。现在把 query 移到 App 中,再通过 props 传给 SearchBox。这样每次按键也会打印 ProductList,尽管列表根本不用这个查询。如果列表确实要按查询来过滤,那提升就是正确的选择,因为那时两个组件都依赖同一个值。

要问的问题是:“谁需要读取这个值?”如果答案是一个组件,state 就留在那里;如果是多个组件,就把它放到它们最近的共同父组件里。

提升得太高的时候

有时最近的共同父组件在组件树的很上层,值必须穿过好几个只负责转手的组件。这就是 prop drilling。几层 props 没问题,也很容易跟踪。当同一个值要穿过很多层,或者几乎每个组件都需要它(当前登录的用户、主题、语言)时,就用 useContext 来读取,而不是手动一层层传。context 改变的是值到达子组件的方式;state 本身仍然放在一个父组件里,所以它依然是被提升的。

常见问题

React 中的状态提升是什么意思?

把一份 state 从使用它的组件中移到它们最近的共同父组件里。父组件持有这份 state,把值以及修改它的函数通过 props 传给子组件。

React 中两个兄弟组件如何共享 state?

兄弟组件无法读取彼此的 state。把 state 放到它们共同的父组件里,把值传给两者,再把 setter(或 onChange 这样的处理函数)传给负责修改它的那个组件。这样两个兄弟组件就根据同一个值来渲染。

子组件如何更新父组件的 state?

父组件把一个函数作为 prop 传下去,比如 onSelect={setSelected} 或 onSelect={(id) => setSelected(id)},子组件调用它即可。state 仍然留在父组件中,子组件只是请求修改。

什么时候不应该提升 state?

只有一个组件用到这份 state 的时候。把它提升得比需要的更高,会让父组件在每次变化时都重新渲染,还会让 props 穿过一堆并不关心它的组件。state 应该尽量靠近使用它的地方。

state 提升得太高时,有什么替代方案?

如果你发现同样的 props 要穿过很多层,那就改用 context(useContext)读取共享的值,或者调整结构,让需要它的组件离得更近。

Coddy 编程语言插图

和 Coddy 一起学编程

开始