0
点赞
收藏
分享

微信扫一扫

基于 props 更新 state


单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。

Props 的只读性

决不能修改自身的 props。所有组件都必须保护它们的 props 不被更改。

State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。state 允许组件随用户操作、网络响应或者其他变化而动态更改输出内容。

让组件在 props 变化时更新 state

在 vue 中我们会这样做,定义一个本地的 data 属性并将这个 prop 用作其初始值,然后通过 watch 监控 prop 变化,然后重复赋值给本地的 data 属性。

props: ['initialCounter'],
data () {
return {
counter: this.props.initialCounter
}
},
watch: {
initialCounter (newCounter) {
if (newCounter !== this.counter) {
this.counter = newCounter
}
}
}

在 react 中,从 16.3 版本开始,当 props 变化时,建议使用新的 ​​static getDerivedStateFromProps​​ 生命周期更新 state。创建组件以及每次组件由于 props 或 state 的改变而重新渲染时都会调用该生命周期:

class ExampleComponent extends React.Component {
// 在构造函数中初始化 state,
// 或者使用属性初始化器。
state = {
counter: this.props.initialCounter,
};

static getDerivedStateFromProps(props, state) {
if (props.initialCounter !== state.counter) {
return { counter: props.initialCounter };
}

// 返回 null 表示无需更新 state。
return null;
}

handleClick = () => {
// 点击之后无法修改 counter 的 bug
this.setState({counter: this.state.counter + 1})
}

render() {
return (
<div onClick={this.handleClick}>{this.state.counter}</div>
);
}
}

​getDerivedStateFromProps​​ 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 ​​componentWillReceiveProps​​ 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时。

基于 props 更新 state,旧的 ​​componentWillReceiveProps​​​ 和新的 ​​getDerivedStateFromProps​​ 方法都会给组件增加明显的复杂性。这通常会导致 bug。

最常见的误解就是 ​​getDerivedStateFromProps​​​ 和 ​​componentWillReceiveProps​​ 只会在 props “改变”时才会调用。实际上只要父级重新渲染时,这两个生命周期函数就会重新调用,不管 props 有没有“变化”。所以,在这两个方法内直接复制 props 到 state 是不安全的。这样做会导致 state 后没有正确渲染。

希望以上能解释清楚为什么直接复制 prop 到 state 是一个非常糟糕的想法。

虽然这个设计就有问题,但是这样的错误很常见,(我就犯过这样的错误)。任何数据,都要保证只有一个数据来源,而且避免直接复制它。

让我们看看一个相关的问题:假如我们只使用 props 中的 counter 属性更新组件呢?

完全可控的组件

阻止上述问题发生的一个方法是,从组件里删除 state。然后传入在父组件中定义的处理函数进行修改。

class ExampleComponent extends React.Component {
render() {
return (
<div onClick={this.props.handleClick}>
{this.props.initialCounter}
</div>
);
}
}

虽然 vue 中没有这个问题,但是建议大家不要在组件里面修改 props,任何数据,都要保证只有一个数据来源,而且避免直接复制它。都必须保护它们的 props 不被更改。

在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。

名词“受控”和“非受控”通常用来指代表单的 inputs,但是也可以用来描述数据频繁更新的组件。用 props 传入数据的话,组件可以被认为是受控(因为组件被父级传入的 props 控制)。数据只保存在组件内部的 state 的话,是非受控组件(因为外部没办法直接控制 state)。

总结

派生状态会导致代码冗余,并使组件难以维护。 确保你已熟悉这些简单的替代方案:

1. 如果你需要执行副作用(例如,数据提取或动画)以响应 props 中的更改,请改用 componentDidUpdate

class ExampleComponent extends React.Component {
state = {
counter: this.props.initialCounter
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}
componentDidUpdate(prevProps) {
if (this.props.initialCounter !== prevProps.initialCounter) {
this.setState({ counter: this.props.initialCounter })
}
}
render() {
return (
<div onClick={this.handleClick}>
{this.state.counter}
</div>
);
}
}

你也可以在 ​​componentDidUpdate()​​ 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props “镜像”给 state,请考虑直接使用 props。

3. 如果只想在 prop 更改时重新计算某些数据,请使用 memoization 帮助函数代替。

仅在输入变化时,重新计算 render 需要使用的值————这个技术叫做 memoization 。(也就是Vue中的计算属性类似)

import memoize from "memoize-one";

class Example extends Component {
// state 只需要保存当前的 filter 值:
state = { filterText: "" };

// 在 list 或者 filter 变化时,重新运行 filter:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);

handleChange = event => {
this.setState({ filterText: event.target.value });
};

render() {
// 计算最新的过滤后的 list。
// 如果和上次 render 参数一样,`memoize-one` 会重复使用上一次的值。
const filteredList = this.filter(this.props.list, this.state.filterText);

return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}

在使用 memoization 时,请记住这些约束:
1、大部分情况下, 每个组件内部都要引入 memoized 方法,已免实例之间相互影响。
2、一般情况下,我们会限制 memoization 帮助函数的缓存空间,以免内存泄漏。(上面的例子中,使用 memoize-one 只缓存最后一次的参数和结果)。

4. 如果你想在 prop 更改时“重置”某些 state,请考虑使组件完全受控或使用 key 使组件完全不受控 代替。

1、完全可控的组件

class ExampleComponent extends React.Component {
render() {
return (
<div onClick={this.props.handleClick}>
{this.props.initialCounter}
</div>
);
}
}

2、有 key 的非可控组件

另外一个选择是让组件自己存储临时的 state。在这种情况下,组件仍然可以从 prop 接收“初始值”,但是更改之后的值就和 prop 没关系了:

class ExampleComponent extends React.Component {
state = {
counter: this.props.initialCounter
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div onClick={this.handleClick}>
{this.state.counter}
</div>
);
}
}

// <ExampleComponent initialCounter={this.state.counter} key={this.state.key} />

我们可以使用 key 这个特殊的 React 属性。当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。

class ExampleComponent extends React.Component {
state = {
counter: this.props.initialCounter
}
handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div onClick={this.handleClick}>
{this.state.counter}
</div>
);
}
}

class App extends React.Component {
state = {
counter: 0,
key: 0
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
key: this.state.key + 1,
})
}
render() {
return (
<div>
<button onClick={this.handleClick} >点击</button>
<ExampleComponent
initialCounter={this.state.counter}
key={this.state.key} />
</div>
);
}
}

如果某些情况下 key 不起作用(可能是组件初始化的开销太大),有两种方法解决这个问题。

方法1:用 prop 的 ID 重置非受控组件

一个麻烦但是可行的方案是在 ​​getDerivedStateFromProps​​ 观察 uuid 的变化:

class ExampleComponent extends React.Component {
// 在构造函数中初始化 state,
// 或者使用属性初始化器。
state = {
counter: this.props.initialCounter,
prevPropsUuid: this.props.uuid
};

static getDerivedStateFromProps(props, state) {
// 观察 uuid 的变化
if (props.uuid !== state.prevPropsUuid) {
return {
counter: props.initialCounter,
prevPropsUuid: props.uuid
};
}

// 返回 null 表示无需更新 state。
return null;
}

handleClick = () => {
this.setState({counter: this.state.counter + 1})
}

render() {
return (
<div onClick={this.handleClick}>={this.state.counter}</div>
);
}
}

class App extends React.Component {
state = {
counter: 0,
key: 0
}
handleClick = () => {
this.setState({
counter: this.state.counter + 1,
key: this.state.key + 1,
})
}
render() {
return (
<div>
<button onClick={this.handleClick} >点击</button>
<ExampleComponent
initialCounter={this.state.counter}
uuid={this.state.key} />
</div>
);
}
}

方法2:使用实例方法重置非受控组件

在组件上使用 ref 可以获取组件实例:

class ExampleComponent extends React.Component {
// 在构造函数中初始化 state,
// 或者使用属性初始化器。
state = {
counter: this.props.initialCounter
};

handleClick = () => {
this.setState({ counter: this.state.counter + 1 })
}

resetCounter (newCounter) {
this.setState({ counter: newCounter });
}

render() {
return (
<div onClick={this.handleClick}>={this.state.counter}</div>
);
}
}

class App extends React.Component {
state = {
counter: 0,
key: 0
}
componentRef = React.createRef()
handleClick = () => {
let newCounter = this.state.counter + 1
this.setState({ counter: newCounter })
this.componentRef.current.resetCounter(newCounter)
}
render() {
return (
<div>
<button onClick={this.handleClick} >点击</button>
<ExampleComponent
initialCounter={this.state.counter}
ref={this.componentRef} />
</div>
);
}
}

refs 在某些情况下很有用,比如这个。但通常我们建议谨慎使用。即使是做一个演示,这个命令式的方法也是非理想的,因为这会导致两次而不是一次渲染。

概括

回顾一下,设计组件时,重要的是确定组件是受控组件还是非受控组件。

不要直接复制 props 的值到 state 中,而是去实现一个受控的组件,然后在父组件里合并两个值。

对于不受控的组件,当你想在 prop 变化(通常是 ID )时重置 state 的话,可以选择一下几种方式:

1、建议: 重置内部所有的初始 state,使用 key 属性
2、选项一:仅更改某些字段,观察特殊属性的变化(比如 props.uuid)。
3、选项二:使用 ref 调用实例方法。

最后提一下,上面是 react 的写法,再介绍一下 vue 的最佳写法使用语法糖 v-model。

<template>
<div class="hello">
<h1 @click="handleClick">{{ value }}</h1>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
props: ['value'],
data() {
return {
counter: this.value
};
},
methods: {
handleClick () {
this.$emit('input', this.value + 1)
}
}
}
</script>

// <HelloWorld v-model="initialCounter"/>


举报

相关推荐

0 条评论