状态管理是一件很有难度的事。一些第三方视图库,比如 React,能够帮助你管理本地组件的状态,但它只能在有限的范围里帮到你,React 仅仅是一个视图层的库。最终你会决定去使用一个更加复杂的状态管理解决方案,比如 Redux,但还有一些我想要在此文中提醒的事项,在你踏上 Redux 的列车以前,这些关于 React 的事项是你应该了解的。
通常大家会同时学习 React 和 Redux,但这会产生一些问题:
- 在仅使用本地状态(this.state)的场景下,大家从不会遇到跨页面状态管理的问题
- 因此不会理解为什么需要一个像 Redux 这样的状态管理库
- 也因此大家会抱怨 Redux 会添加过多的模板
- 不会在 React 里学习管理本地状态
- 因此大家会管理(以及搞乱) Redux 状态容器里的所有状态
- 所以不会使用本地状态管理
因为这些问题,你通常会被建议先学习 React,再在晚点的时候把可选项 Redux 接入你的技术栈,但只是当你遇到跨页面的状态管理的时候才去选择接入 Redux。这些跨页面问题只适用于大型应用。一般来说你不需要状态管理库, 比如 Redux,这本书 The Road to learn React 阐述了怎样不使用额外依赖如 Redux 而只用简单的 React 来搭建一个应用。
不过,现在你决定了要跳上 Redux 的列车,所以就有了我的这张清单,它包含了在使用 Redux 前你所应该知道的 React 的内容。
React 中的本地状态成为第二天性
之前提到过的最重要的建议是先学习 React,所以你无法避免在你的组件里用 this.setState()
和 this.state
来操作本地状态。你应该能够自如地使用它:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
}
render() {
return (
<div>
Counter: {this.state.counter}
<button
type="button"
onClick={() => this.setState({ counter: this.state.counter 1 })}
/>
</div>
);
}
}
一个 React 组件有一个在 constructor 里定义的初始状态,之后,你可以用 this.setState()
方法更新这个状态。这个状态对象的更新是一次浅合并(shallow merge),所以你可以部分更新这个本地状态对象,而它仍将保留其他状态对象里的属性原封不动。一旦状态被更新,那么组件会重新渲染,在之前的例子里面,它会显示更新值:this.state.counter
。基本上,这就是一个 React 非定向数据流的闭环。
React 的函数式本地状态
this.setState()
方法会异步地更新本地状态,所以,你不能依赖状态更新的时机,当然它最终是会更新的。对于大多数情形来说,完全没问题。
但是,想你一下,当你计算组件的下一个状态时,你得依赖当前的本地状态,大概就像之前的例子做的那样:
代码语言:javascript复制this.setState({ counter: this.state.counter 1 });
这个用来计算的本地状态 (this.state.counter) 只是一个适时的快照,因此当你调用 this.setState()
更新状态的时候,你的本地状态改变还在异步执行进入以前,那么你将操作一个老旧的状态。第一次遇到的时候,也许很难发现。这就是为什么一个代码片段反复强调的:
this.setState({ counter: this.state.counter 1 }); // this.state: { counter: 0 }
this.setState({ counter: this.state.counter 1 }); // this.state: { counter: 0 }
this.setState({ counter: this.state.counter 1 }); // this.state: { counter: 0 }
// updated state: { counter: 1 }
// instead of: { counter: 3 }
如我所见,当你的状态更新需要依赖本地状态的时候,你无法通过本地状态来更新,这会导致 bug,那也就是为什么存在第二种方式来更新你的 React 本地状态:
this.setState()
函数采取另一种方式,以函数来替代对象。它接受的这个函数(参数) 的函数签名里,包含有 this.setState()
异步执行后的本地状态。这是一个回调函数,这个回调执行的是在(异步执行后的)那个时点正确的状态,所以是可靠的:
this.setState(previousState => ({ counter: previousState.counter 1 }));
以这种方式,当你需要依赖之前状态的时候,你能够一直通过函数(入参)来使用 this.setState()
,而不是一个对象
而且,这同样可以应用于依赖 props 的更新。当从父组件中接收到的 props 在异步执行前就已经改变的时候,这些 props 同样也会变成过期的状态。因此,this.setState()
接受的函数签名中把 props 作为第二个参数:
this.setState((prevState, props) => ...);
以这种方式,你就能确保你能够依赖正确的 state 和 props 来更新状态。
代码语言:javascript复制this.setState((prevState, props) => ({ counter: prevState.counter props.addition }));
另一个好处是,当你使用函数的时候,你可以在一个单独的地方测试你的状态。简单地抽取 this.setState(fn)
中使用的回调函数,单独拿出来,然后 export 出来使其可测。它应该是个纯函数,在里面你可以简单地依靠输入来测试输出。
React 的状态(State)和属性(Props)
状态是在组件中管理的,它能被当作 props 传递给其他组件,这些组件可以使用这些 props,或者把它更进一步传给它们(这些组件)的子组件。而且,子组件可以从他们父组件的 props 里接收回调函数,这些函数可以用来改变父组件的本地状态。一般来说,props 沿着组件树向下流动,状态由组件单独管理,函数可以向上冒泡以改变组件中的状态。更新后的状态可以重新作为 props 往下传递。
一个组件可以管理非常多的状态,把它作为 props 向下传递给它的子组件,并且把一些函数也按这种方式向下传递以使得子组件获得再次改变父组件中状态的能力。
但是,子组件不关心 props 中接收函数的来源或者功能,这些函数可以更新父组件中的状态,或者做些其他的事情。子组件只是去执行它们,这同样适用于 props。一个组件不知道它所接收的 props 是否是 props、state 亦或是从父组件中衍生出来的其他属性(other properties),子组件只是使用这些 props。
了解 props 和 state 的概念非常重要。所有你在组件树中使用的属性都能被分成 state 和 props (以及从 state/props 衍生出来的其他属性)。所有需要交互的内容在 state 里面,其他的作为 props 向下传递。
在依赖一个复杂的状态管理库以前,你应该已经试过把你的 props 从一些组件中向下传递给组件树。当你把 props 传给好几个组件,但却没有在组件里使用这些 props,仅仅是在最后一个子组件里使用的时候,你应该知道"需要有更好的方式来做这件事"的感觉。
提升 React 的状态(state)
你是否已经提升过你的本地状态层?这是在 React 中让你的本地状态管理能跨页面的最重要的策略。状态层可以被提升或者下降。
你可以把你的本地状态向下提升以使它对其他组件来说访问权限更低。想你一下你有一个组件 A,它是组件 B 和 C 的父组件,B 和 C 是 A 的子组件而且它们是兄弟组件。组件 A 是唯一的管理本地状态的组件,但它会把本地状态作为 props 向下传递给子组件,而且,它会把必需的函数传下去,从而使得 B 和 C 能够改变自己在 A 中的状态。
代码语言:javascript复制 ----------------
| |
| A |
| |
| Stateful |
| |
-------- -------
|
--------- -----------
| |
| |
-------- ------- -------- -------
| | | |
| | | |
| B | | C |
| | | |
| | | |
---------------- ----------------
现在,组件 A 中一半的状态会作为 props 被 C 使用而不被 B 使用,而且,C 的 props 里接收函数以改变只被 C 使用的 A 中的状态。你能看到,组件 A 代表组件 C 在管理状态。在大多数情况下,用一个组件来管理其所有子组件的状态是可行的,但如果除此以外,在 A 和 C 之间还有几个其他组件,所有需要从组件 A 拿到的 props 需要通过组件树向下遍历最终到达组件 C,这时候组件 A 还在为了组件 C 管理其状态。
代码语言:javascript复制 ----------------
| |
| A |
| |
| |
| Stateful |
-------- -------
|
--------- -----------
| |
| |
-------- ------- -------- -------
| | | |
| | | |
| B | | |Props |
| | | v |
| | | |
---------------- -------- -------
|
-------- -------
| |
| |
| |Props |
| v |
| |
-------- -------
|
-------- -------
| |
| |
| C |
| |
| |
----------------
这是一个将 React 状态向下提升的完美用例。当组件 A 只管理代表组件 C 的状态的时候,这一部分状态可以在组件 C 中单独管理。在这方面,它可以自我管理。当你把组件 C 中的状态向下提升的时候,所有必需的 props 没有必要遍历整棵组件树。
代码语言:javascript复制 ----------------
| |
| A |
| |
| |
| Stateful |
-------- -------
|
--------- -----------
| |
| |
-------- ------- -------- -------
| | | |
| | | |
| B | | |
| | | |
| | | |
---------------- -------- -------
|
-------- -------
| |
| |
| |
| |
| |
-------- -------
|
-------- -------
| |
| |
| C |
| |
| Stateful |
----------------
除了组件 A 中的状态被清理了以外,它只管理自身的、以及最近子组件的必需状态。
React 中的状态提升也可以向另一个方向:将状态向上提升。想像一下,你还有一个作为父组件的组件 A,以及其子组件 B 和 C,AB 或 AC 间无论有多少个组件。但是这一次,C 已经管理它自己的状态了。
代码语言:javascript复制 ----------------
| |
| A |
| |
| |
| Stateful |
-------- -------
|
--------- -----------
| |
| |
-------- ------- -------- -------
| | | |
| | | |
| B | | |
| | | |
| | | |
---------------- -------- -------
|
-------- -------
| |
| |
| C |
| |
| Stateful |
----------------
如果组件 B 需要在 C 中管理的状态呢?这部分是无法共享的,因为状态只能 props 向下传递。这就是为什么你需要把状态向上提升。你可以把来自组件 C 的状态向上提升,直到你有一个对于 B 和 C 来说的公共父组件(本例中是 A)。如果所有在 C 中管理的状态都是 B 中需要的,那么 C 甚至能变成一个无状态(stateless)组件。这些状态可由 A 管理,但被 B 和 C 共享。
代码语言:javascript复制 ----------------
| |
| A |
| |
| |
| Stateful |
-------- -------
|
--------- -----------
| |
| |
-------- ------- -------- -------
| | | |
| | | |
| B | | |Props |
| | | v |
| | | |
---------------- -------- -------
|
-------- -------
| |
| |
| C |
| |
| |
----------------
将状态向上或向下提升能够使你在 React 中跨页面管理你状态。当更多的组件对某个特定状态关注的时候,你可以把状态向上提升直到它们的某个需要这些状态访问权限的公共父组件。另外,本地状态管理是可维护的,因为一个组件只管理它所需要的所有状态。如果状态没有在该组件或其子组件中用到,它就应该被向下提升到与其相关的需要这个状态的组件上。
你可以在 官方文档 读到更多关于提升 React 状态的部分。
React 的高阶组件
高阶组件 (HOCs) 是 React 中的一种高级模板。你可以使用高阶组件来将功能提取出来,但是在多个组件中作为可选功能参数来重用它。一个高阶组件接受组件和可选配置作为输入,然后返回该组件的加强版本。这是建立在 Javascript 高阶函数的基础上:返回函数的函数。
如果你不熟悉高阶组件,我推荐你阅读 React 高阶组件入门介绍。其中会讲授使用 React 条件渲染的 React 高阶组件内容。
高阶组件在之后会非常重要,因为你将会在使用像 Redux 之类的库的时候遇到它们。当像 Redux 这样的库将状态管理和 React 视图层“连接”(connect 方法,react-redux 中将组件和 state 连接的重要方法,译者注) 起来的时候,你会经常使用高阶组件来完成这部分连接的工作 (在 react-redux中连接高阶组件)。
这同样适用像 MobX 之类的其他状态管理库。高阶组件在这些库中被用来将状态管理层粘合到视图层上去(另一个强大的库叫 recompose,类似高阶组件的思想,用来向组件注入增强功能,译者注)。
React 的上下文(Context)
React 的上下文 很少使用。我不会建议使用它,因为它的 API 不稳定,而且它给你的应用增加了很多可能的复杂性。但是,理解它的作用还是有必要的。
那么为什么你要花时间了解这块内容呢? React 的上下文是用来在组件树中向下隐式传递属性的。你可以在父组件的某个地方将属性声明成上下文,然后在组件树下层子组件的某个地方获得这个属性。但整个过程不需要在组件树中,在生产状态的父组件和使用状态的子组件间,显式地向下传递 props。这是一个可以向下到达组件树的不可见容器,那么老问题又来了,为什么应该关注它?
通常,当使用一个复杂状态管理库的时候,比如 Redux 和 MobX,你在某个地方把状态管理层连接到 React 视图层上,这就是为什么你在 React 中提及高阶组件。这种连接应该允许你访问并修改状态,状态自身通常由某种状态容器来管理。
但你是怎么才能让这个状态容器能够被所有连接到状态的 React 组件能够访问呢?这会由 React 上下文来完成。在你的顶层组件中,一般来说是你的 React 根组件,你需要在 React 上下文声明状态容器,使得这个容器对于组件树中的每一个组件都是可访问的。整个这个部分是由 React Provider 模板 实现的。
最后,这并不意味着,当你使用 Redux 之类的库的时候,你需要自己处理 React 的上下文,这类库已经给你提供了使得状态容器在所有组件中可访问的解决方案。但是,当你的状态能够被不同的组件访问,而不用担心状态容器来自哪里的时候,这种底层机制,为什么它能工作,是很值得了解的事实。
React 的状态组件(Stateful Components)
React 有两种组件声明方式:ES6 class 组件和函数式无状态组件(stateless componenet)。函数式无状态组件只是一个接收 props 然后输出 JSX 的函数。它既不保存任何状态,也无法使用 React 的生命周期方法。顾名思义,它就是无状态的。
代码语言:javascript复制function Counter({ counter }) {
return (
<div>
{counter}
</div>
);
}
另一方面,React 的 ES6 class 组件可以有本地状态和生命周期方法,这些组件能访问 this.state
和 this.setState()
方法。这意味着 ES6 class 组件是有状态的组件。但它们不需要使用本地状态,所以它可以是无状态的。通常无状态的 ES6 class 组件使用生命周期方法以证明它们是 class。
class FocusedInputField extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.input.focus();
}
render() {
return (
<input
type="text"
value={this.props.value}
ref={node => this.input = node}
onChange={event => this.props.onChange(event.target.value)}
/>
);
}
}
结论是只有 ES6 class 组件可以是有状态的,但它们也能是无状态的。单独的函数式无状态组件总是无状态的。
除此以外,高阶组件也可以用来往 React 组件中添加状态。你可以编写自己的高阶组件来管理状态,或者是使用 recompose 的高阶组件 withState 这一类的库。
代码语言:javascript复制import { withState } from recompose;
const enhance = withState('counter', 'setCounter', 0);
const Counter = enhance(({ counter, setCounter }) =>
<div>
Count: {counter}
<button onClick={() => setCounter(n => n 1)}>Increment</button>
<button onClick={() => setCounter(n => n - 1)}>Decrement</button>
</div>
);
当使用 React 高阶组件的时候,你可以选择把本地状态加到 React 的任意组件里去。
容器和表现器模板(presenter pattern)
容器和表现器模板是在 Dan Abramov 的一篇博客中逐步流行起来的。如果你对它不熟悉,现在是你深入了解的机会了。大概来说,它把组件分成两种类型:容器 (container) 和表现器 (presenter)。容器组件描述了如何工作,而表现器组件则描述了外观形态。一般来说,这表示容器组件是一个 ES6 class 组件,例如因为它管理本地状态,而表现器组件则是一个函数式无状态组件,例如因为它只显示 props 并使用几个从父组件传下来的函数。
在更深入 Redux 以前,理解这种模式背后的原理很有必要。使用状态管理库的时候,你会把组件“连接”到状态上。这些组件不关心外观形态,但更关心如何工作,因此这些组件是容器组件。更具体的说,你会经常听到术语 connected componenets 当某个组件已经连接上状态管理层的时候。
MobX 还是 Redux?
在所有状态管理库中,Redux 是最受欢迎的,但 MobX 也是一个优秀的备选项。这两个库分别遵循不同的哲学和编程范式。
在你决定使用其中之一之前,明确你是否了解本文涵盖的所有关于 React 的内容。你应该能够自如地使用本地状态管理,而且还要知道足够多的 React 知识,以便将不同理念应用到跨页面状态管理中。另外,明确你需要跨页面状态管理解决方案是因为你的应用将在未来不断扩展。也许提升你的状态,或是用 React Provider 模板完成一次 React 上下文就会解决你问题。
那么如果你决定向 Redux 或 MobX 再迈出一步,你可以阅读下面的文章以做出一个更复杂的决定:Redux or MobX: An attempt to dissolve the Confusion。该文对两个库给出了一份有用的比较,并且给出了一些学习和应用它们的建议。或者去看看 Tips to learn React Redux,通过这篇文章开始 Redux 的旅程。