React 基于 props 更新 state

发布于 2022-09-21 12:44:12 字数 6836 浏览 232 评论 0

单向数据流

所有的 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.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。

最常见的误解就是 getDerivedStateFromPropscomponentWillReceiveProps 只会在 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 不被更改。

总结

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

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>
    );
  }
}

上面是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"/>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

少女净妖师

暂无简介

0 文章
0 评论
673 人气
更多

推荐作者

小瓶盖

文章 0 评论 0

wxsp_Ukbq8xGR

文章 0 评论 0

1638627670

文章 0 评论 0

仅一夜美梦

文章 0 评论 0

夜访吸血鬼

文章 0 评论 0

近卫軍团

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文