如何正确地使用 State
React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。
也就是说,只需更新组件的 state,然后就可以根据新的 state 重新渲染用户界面。
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
https://codepen.io/gaearon/pen/amqdNA?editors=0010
不要直接修改 State
例如,此代码不会重新渲染组件:
// Wrong
this.state.comment = 'Hello';
应该使用 setState():
// Correct
this.setState({comment: 'Hello'});
构造函数是唯一可以给 this.state 赋值的地方。
State 的更新可能是异步的
出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
例如,在计时器中此代码可能会无法更新counter:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
State 的更新会被合并
当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。
所以当你的 state 包含多个变量时,你可以分别调用 setState() 来单独地更新它们。
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
数据是向下流动的
不管是父组件或是子组件,都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。
这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。
组件可以选择把它的 state 作为 props 向下传递到它的子组件中:
FormattedDate 组件会在其 props 中接收参数 date,但是组件本身无法知道它是来自于父组件的 state,或是props,还是手动输入的。
这通常会被叫做“自上而下”或是“单向”的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件,只能向下流动。
组件的生命周期
我们可以为 class 组件声明一些特殊的方法,当组件挂载或卸载时就会去执行这些方法。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
}
componentWillUnmount() {
}
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
每个组件都包含“生命周期方法”,你可以重写这些方法,以便于在运行过程中特定的阶段执行这些方法。
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
constructor(): 在 React 组件挂载之前,会调用它的构造函数。
getDerivedStateFromProps(): 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
过时方法:componentWillMount()
render():render() 方法是 class 组件中唯一必须实现的方法。
componentDidMount(): 在组件挂载后(插入 DOM 树中)立即调用。
render() 方法是 class 组件中唯一必须实现的方法,其他方法可以根据自己的需要来实现。
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps() :在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
getDerivedStateFromProps返回一个对象来更新 state,或者返回 null 来表示新的 props 不需要任何 state 的更新。
class Example extends React.Component {
static getDerivedStateFromProps(props, state) {
// ...
return null;
}
}
getDerivedStateFromProps的存在只有一个目的:让组件在 props 变化时更新 state。
要保守使用派生 state。大部分使用派生 state 导致的问题,不外乎两个原因:1,直接复制 props 到 state 上;2,如果 props 和 state 不一致就更新 state。
class EmailInput extends Component {
state = { email: this.props.email };
render() {
return ;
}
handleChange = event => {
this.setState({ email: event.target.value });
};
componentWillReceiveProps(nextProps) {
// 这会覆盖所有组件内的 state 更新!
// 不要这样做。
this.setState({ email: nextProps.email });
}
}
state 的初始值是 props 传来的,当在 里输入时,修改 state。但是如果父组件重新渲染,我们输入的所有东西都会丢失!
class EmailInput extends Component {
state = {
email: this.props.email
};
componentWillReceiveProps(nextProps) {
// 只要 props.email 改变,就改变 state
if (nextProps.email !== this.props.email) {
this.setState({
email: nextProps.email
});
}
}
// ...
}
以上均为反面模式,建议的模式一个是使用完全可控的组件。从组件里删除 state,直接使用props.email,但是如果我们仍然想要保存临时的值,则需要父组件手动执行保存这个动作。
另外一个选择是让组件自己存储临时的 email state。在这种情况下,组件仍然可以从 prop 接收“初始值”,但是更改之后的值就和 prop 没关系了。
class EmailInput extends Component {
state = { email: this.props.defaultEmail };
handleChange = event => {
this.setState({ email: event.target.value });
};
render() {
return ;
}
}
然后使用key这个特殊的 React 属性,当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。每次 ID 更改,都会重新创建 EmailInput ,并将其状态重置为最新的 defaultEmail 值。
过时方法:componentWillReceiveProps()
shouldComponentUpdate() :当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。
过时方法:componentWillUpdate()
render() :render() 方法是 class 组件中唯一必须实现的方法。
getSnapshotBeforeUpdate() :在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
{/* ...contents... */}
);
}
}
componentDidUpdate() :在更新后会被立即调用。首次渲染不会执行此方法。
当组件从 DOM 中移除时会调用如下方法:
componentWillUnmount():在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
static getDerivedStateFromError() :此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显降级 UI
return { hasError: true };
}
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return Something went wrong.
;
}
return this.props.children;
}
componentDidCatch() :此生命周期在后代组件抛出错误后被调用。 它接收两个参数:
-
error
—— 抛出的错误。 -
info
—— 带有componentStack
key 的对象,其中包含有关组件引发错误的栈信息
componentDidCatch(error, info) {
//
logComponentStackToMyService(info.componentStack);
}
逐步迁移过程
过时的组件生命周期往往会带来不安全的编码实践,具体函数如下:
componentWillMount()
componentWillReceiveProps()
componentWillUpdate()
这些生命周期方法经常被误解和滥用;此外,在异步渲染中,它们潜在的误用问题可能更大。
16.3:为不安全的生命周期引入别名,UNSAFE_componentWillMount、UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate。(旧的生命周期名称和新的别名都可以在此版本中使用。)
并且引入两个新的生命周期,静态的 getDerivedStateFromProps 和 getSnapshotBeforeUpdate。
**16.x **:为 componentWillMount、componentWillReceiveProps 和 componentWillUpdate 启用废弃告警。(旧的生命周期名称和新的别名都将在这个版本中工作,但是旧的名称在开发模式下会产生一个警告。)
17.0:删除 componentWillMount、componentWillReceiveProps 和 componentWillUpdate。(在此版本之后,只有新的 “UNSAFE_” 生命周期名称可以使用。)