【React】为什么我不再使用setState?

7,930 阅读4分钟

几个月前,我开始停止使用React的 setState 。我并不是不再需要组件状态,而且不再用React来管理我的组件状态。

setState对于新手来说不是很友好,即使是有经验的React程序员在使用setState时,也很容易出bug,比如:

Bug产生的原因是忘记了React的state是异步的;从日志打印延迟可以看出来。

React的官方文档已将把使用setState可能会出现的所有问题都总结了:

注意:

永远不要直接修改this.state,需要通过调用this.setState方法来替换你修改后的值。把this.state当做不可变数据来处理。

setState()不会马上去改变this.state,而是会排队等待处理,所以当你调用setState()后访问this.state,有可能会返回旧的state

当你调用setState()时,无法保证是同步执行的,因为为了保证性能可能会被批处理。

setState()总是会触发render()进行重新渲染,除非在shouldComponentUpdata()控制了渲染逻辑。如果用了可变数据结构以及在shouldComponentUpdata()中并没有控制渲染逻辑,调用setState()将不会触发重新渲染渲染。

总的来说,使用setState会带来三个问题:

1. setState是异步的

许多开发人员起初并没有意识到这一点,当你设置了新的state,却发现没有变化,这是setState最诡异的地方,因为setState调用的时候看起来并不是异步。比如下面的代码:


class Select extends React.Component {

  constructor(props, context) {

    super(props, context)

    this.state = {

      selection: props.values[0]

    };

  }

  render() {

    return (

      <ul onKeyDown={this.onKeyDown} tabIndex={0}>

        {this.props.values.map(value =>

          <li

            className={value === this.state.selection ? 'selected' : ''}

            key={value}

            onClick={() => this.onSelect(value)}

          >

            {value}

          </li> 

        )}  

      </ul>

    )

  }

  onSelect(value) {

    this.setState({

      selection: value

    })

    this.fireOnSelect()

  }

  onKeyDown = (e) => {

    const {values} = this.props

    const idx = values.indexOf(this.state.selection)

    if (e.keyCode === 38 && idx > 0) { /* up */

      this.setState({

        selection: values[idx - 1]

      })

    } else if (e.keyCode === 40 && idx < values.length -1) { /* down */

      this.setState({

        selection: values[idx + 1]

      })  

    }

    this.fireOnSelect()

  }

  fireOnSelect() {

    if (typeof this.props.onSelect === "function")

      this.props.onSelect(this.state.selection) /* not what you expected..*/

  }

}

ReactDOM.render(

  <Select 

    values={["State.", "Should.", "Be.", "Synchronous."]} 

    onSelect={value => console.log(value)}

  />,

  document.getElementById("app")

)

乍一看没什么问题,但是这个select组件有一个bug,上面的git图已经很好的证明了。onSelect()方法触发时总是得到前一个state.selection的值,因为setState还没有完成,fireOnSelect就被调用了。我认为应该把setState重新命名为scheduleState或者要求传入回调。

这个bug很容易修复,棘手的地方在于你很难发现它。

2. setState引起没有必要的渲染

setState的第二个问题在于它总是会触发重新渲染,很多时候这种渲染是没有必要的。你可以通过printWasted(React提供的性能工具)来检测它会在什么时候重新渲染。从三个方面来粗略的讲为什么重新渲染有时候是没有必要的:

  • 当新的state和旧的state是一样的。可以通过shouldComponentUpdate来解决,也可以用纯渲染库来解决。

  • 只有某些时候state的改变才和渲染有关系。

  • 第三,某些时候state和视图层一点关系都没有,比如用来管理事件的监听器,定时器等相关的state

3. setState不可能管理所有组件的状态

接着上面最后那点说,不是所有的组件状态都需要通过setState来储存和更新。大多数复杂的组件通常需要管理定时器循环,接口请求,事件等等。如果用setState来管理,不仅仅会引起没有必要渲染,而且会造成死循环。

用MobX来管理组件状态

代码如下:


import {observable} from "mobx"

import {observer} from "mobx-react"

@observer class Select extends React.Component {

  @observable selection = null; /* MobX managed instance state */

  constructor(props, context) {

    super(props, context)

    this.selection = props.values[0]

  }

  render() {

    return (

      <ul onKeyDown={this.onKeyDown} tabIndex={0}>

        {this.props.values.map(value =>

          <li

            className={value === this.selection ? 'selected' : ''}

            key={value}

            onClick={() => this.onSelect(value)}

          >

            {value}

          </li> 

        )}  

      </ul>

    )

  }

  onSelect(value) {

    this.selection = value

    this.fireOnSelect()

  }

  onKeyDown = (e) => {

    const {values} = this.props

    const idx = values.indexOf(this.selection)

    if (e.keyCode === 38 && idx > 0) { /* up */

      this.selection = values[idx - 1]

    } else if (e.keyCode === 40 && idx < values.length -1) { /* down */

      this.selection = values[idx + 1]

    }

    this.fireOnSelect()

  }

  fireOnSelect() {

    if (typeof this.props.onSelect === "function")

      this.props.onSelect(this.selection) /* solved! */

  }

}

ReactDOM.render(

  <Select 

    values={["State.", "Should.", "Be.", "Synchronous."]} 

    onSelect={value => console.log(value)}

  />,

  document.getElementById("app")

)

效果如下:

用同步的组件状态的机制没有出现意想不到的bug。

上面的代码片段不仅简洁美观,MobX还解决了setState的全部问题:

改变state马上就反映到了组件state上。这让代码逻辑和代码重用变得更简单。你不用去担心state是否更新了。

MobX在运行时候确定哪些state与视图层关联,暂时与视图层无关的State不会引起重新渲染,直到再次关联。

所以可渲染和不可渲染状态是统一处理的。

此外,直接修改state对象的低级错误不能再犯了。还有,不要担心是否还能用shouldComponentUpdate 和或者PureRenderMixin方法,MobX也很好的处理了。最后,你也许会想,我如何等到setState处理完成呢,你可以使用compentDidUpdate生命周期。

最后:

我已经停止使用React来管理组件状态了,而是用MobX代替。现在React成了“正真”的视图层:)。

以下是实现的Dome:

原文:medium.com/@mweststrat…