使用计算字段反应状态

Posted

技术标签:

【中文标题】使用计算字段反应状态【英文标题】:React state with calculated fields 【发布时间】:2018-08-02 13:12:27 【问题描述】:

我有一个反应组件,它具有属性和状态。 state 的某些字段包含输入数据(从输入控件提升),但 state 中也有字段必须根据当前 State 和 Props 计算:

问题:更新状态计算字段的最佳方法是什么(基于状态和道具的其他字段)?

丑陋的做法:

componentDidUpdate()
    this.setState(calculatedField:calculate(this.props,this.state))) 

在这种情况下,我会获得无限循环的更新,或者在最好的情况下(如果我使用 PureComponent)双重渲染调用。

迄今为止我找到的最佳解决方案(但仍然很丑): 就是在 state 中创建一个calculated 对象,其中包含计算字段并在 componentWillUpdate 中更新,避免 setState:

componentWillUpdate(nextProps,nextState)
   nextState.calculated.field1=f(nextProps,nextState)

class ParentComponent extends React.Component 
  constructor(props, ctx) 
    super(props,ctx)
    this.state=A:"2"
  

  render() 
    console.log("rendering ParentComponent")
    return <div>
      <label>A=<input value=this.state.A onChange=e=>this.setState(A:e.target.value) /></label> (stored in state of Parent component)
      <ChildComponent A=this.state.A />
    </div>
  


class ChildComponent extends React.PureComponent 
  constructor(props,ctx) 
    super(props,ctx);
    this.state=
      B:"3",
      Calculated:
    
  

  render() 
    console.log("rendering ChildComponent")
    return <div>
      <label>B=<input value=this.state.B onChange=e=>this.setState(B:e.target.value) /></label> (stored in state of Child component state)
      <div>
        f(A,B)=<b>this.state.Calculated.result||""</b>(stored in state of Child component)
        <button onClick=e=> this.setState(Calculated:result:new Date().toTimeString()) >Set manual value</button>
      </div>
    </div>
  

  componentWillUpdate(nextProps, nextState) 
    this.state.Calculated.result = getCalculatedResult(nextProps.A, nextState.B)
  

  componentWillReceiveProps(nextProps) 
    this.state.Calculated.result = getCalculatedResult(nextProps.A, this.state.B)
  

  componentWillMount() 
    this.state.Calculated.result = getCalculatedResult(this.props.A, this.state.B)
  


function getCalculatedResult(a,b) 
  const aNum = Number(a)||0
  const bNum = Number(b)||0;
  const result = (aNum*bNum).toString();
  return result;


ReactDOM.render(<ParentComponent/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>

这也是一个丑陋的解决方案,React 不建议改变状态以避免 setState。那么什么是正确的解决方案呢?

注意:

在我的实际应用程序中,我无法在渲染过程中每次都重新计算 f(a,b),因为它实际上是一个复杂的对象,所以我需要以某种方式缓存它,最好的方法是在状态中。

【问题讨论】:

我认为你不应该在渲染中使用setState,绑定一个方法并在那里设置状态。 @PaulRedmond,不确定你的意思。我不在渲染中调用 setState。你能提供一些代码示例吗? 您可以将计算结果保存在this.calculated 而不是this.state。它是依赖数据。所有导致更新和渲染的数据都已经在 state 和 props 中。 @PhilippMunin 你最后做了什么? 【参考方案1】:

您的第一次尝试是解决此问题的正确方法。但是,您需要添加检查以查看状态是否实际发生了变化:

componentDidUpdate(prevProps, prevState)
    if(prevState.field !== this.state.field)
        this.setState(calculatedField:calculate(this.props,this.state))) 
    


shouldComponentUpdate(nextProps, nextState) 
    return this.state.calculatedField !== nextState.calculatedField

您需要检查您在计算方法中使用的状态和道具,并确保它们在再次更新状态之前已更改。这将防止无限循环。

【讨论】:

但不会阻止双重渲染 - 在 componentDidUpdate 中调用 setState 会触发下一个更新和渲染周期 @PhilippMunin 是的,它会再次触发componentDidUpdate,但这就是您使用检查来了解是否需要再次更新的原因。如果您还想防止重新渲染,可以将其与 shouldComponentUpdate 结合使用。 @PhilippMunin 更新了答案以说明我所描述的内容。如果您可以为您的问题提供一个有效的解决方案,我很乐意对其进行调整以使其正常工作。【参考方案2】:

我不建议您将计算的值存储在您的州内。我的做法会更像这样:

import PropTypes from 'prop-types';
import React from 'react';

class Component extends React.Component 
  static defaultProps =  value: 0 ;

  static propTypes =  value: PropTypes.number ;

  state =  a: 0, b: 0 ;

  result = () => this.state.a + this.state.b + this.props.value;

  updateA = e => this.setState( a: +e.target.value );

  updateB = e => this.setState( b: +e.target.value );

  render() 
    return (
      <div>
        A: <input onChange=this.updateA value=this.state.a />
        B: <input onChange=this.updateB value=this.state.b />
        Result: this.result()
      </div>
    );
  

将计算存储在您的状态中的问题在于,计算可能会被多个来源改变。如果您使用我的解决方案,那么任何东西都无法覆盖计算而不使用正确的函数来计算它们。

【讨论】:

在我的真实应用程序中,我无法在渲染过程中每次都重新计算 f(a,b),因为它实际上是一个复杂的对象,所以我需要以某种方式缓存它,最好的方法是在 state 中。我在问题中添加了注释。【参考方案3】:

不要在您的状态中包含多余的信息。

一个简化的例子是在你的州有firstNamelastName。如果我们想在您的 render 方法中显示全名,您只需这样做:

render() 
    return <span>`$this.state.firstName $this.state.lastName`</span>


我喜欢这个例子,因为很容易看出在我们的状态中添加一个 fullName 是不必要的,它只包含 $this.state.firstName $this.state.lastName。每次我们的组件渲染时我们都会进行字符串连接,我们可以接受,因为它是一种廉价的操作。

在您的示例中,您的计算成本很低,因此您也应该在 render 方法中进行计算。

【讨论】:

我认为很明显,我提供的示例非常简化,我这样做只是为了演示问题。在我的实际应用中,计算出来的字段值是一个由控件的 props 和 state 生成的复杂对象。不过感谢您的回答。【参考方案4】:

您可以将计算结果保存在this.calculated 而不是this.state。它是依赖数据。所有导致更新和渲染的数据都已经在 state 和 props 中。

class Component extends React.Component 
  constructor(props) 
    super(props)
    state = 
      b: 0
    
  

  updateThis = (event) => 
    this.setState( b: event.target.value );
  

  componentWillUpdate(nextProps,nextState)
    this.calculated.field1=f(nextProps.a, nextState.b)
  

  render() 
    return (
      <form>
        A = <input onChange=this.props.updateParent value=this.props.a /> <br>
        B = <input onChange=this.updateThis value=this.state.b /> <br>
        f(A,B) = this.calculated.field1 <br>
      </form>
    );
  


class ParentComponent extends React.Component 
  constructor(props) 
    super(props)
    state = 
      a: 0
    
  

  render() 
     return (
       <Component
         updateParent=event=>this.setState(a: event.target.value)
         a=this.state.a
       />
     
  

【讨论】:

我目前正在采用另一种方法来解决这种情况(如果相关的道具发生了变化,请检查“componentDidUpdate”和“setState”),但我实际上认为这可能会更好。 componentDidUpdate 中调用setState 会导致仅用于状态更改的一次渲染。 componentWillUpdate 提供最大的灵活性,但目前 react 建议在这种情况下使用 getDerivedStateFromProps()【参考方案5】:

看起来“状态”是存储您需要在渲染函数上使用的所有内容(甚至是计算值)的地方,但通常我们会遇到您描述的问题。

React 16.3 以来,已经以 static getDerivedStateFromProps (nextProps, prevState)“生命周期挂钩”的方式提供了一种针对这种情况的新方法。

如果你还没有,你应该至少更新到这个版本,follow the advice given by the React Team on their blog。

Here is the official documentation for this functionality.

这里的问题是这个函数在每次渲染之前被调用,并且是“静态的”你不能访问当前实例之前的道具,这通常需要决定是否必须再次生成计算值(我想这个是您的情况,正如您所说,您的计算过程很繁重)。在这种情况下,React 团队建议将相关 props 的值存储在 state 中,以便与新的值进行比较:

if (nextProps.propToCompute !== prevState.propToComputePrevValue) 
  return 
    computedValue: Compute(nextProp.propToCompute),
    propToComputePrevValue: nextProps.propToCompute
  ;

return null;

【讨论】:

【参考方案6】:

如果您使用的是 React 16.8.0 及更高版本,则可以使用 React hooks API。我认为这是您可能需要的 useMemo() 钩子。例如:

import React,  useMemo  from 'react'

const MyComponent = ( ...props ) => 
  const calculatedValue = useMemo(
    () => 
      // Do expensive calculation and return.
    ,
    [a, b]
  )

  return (
    <div>
       calculatedValue 
    </div>
  )

更多详情请参考React documentation

【讨论】:

这应该是公认的答案。它解决了重新计算的性能问题,这是引发原始问题的原因。它比将其存储在状态中要好,因为正如 Ole 所提到的,它可以保护您免受直接更改计算值的影响。

以上是关于使用计算字段反应状态的主要内容,如果未能解决你的问题,请参考以下文章

使用查询中的计算字段更新表

arcgis字段计算器

反应 eslint 错误中未使用的状态字段

当状态改变时,对文本字段做出反应 onChange 更新整个组件

使用状态自定义样式的反应+样式化组件

输入字段失去对每个字符类型的关注 - 反应