React state with calculated fields

2020-07-09 10:25发布

I have a react component, which has properties and state. Some fields of state contain input data (uplifted from input control), but there is also fields in the state that must be Calculated based on current State and Props:

enter image description here

The question: what is the best way to update calculated fields of the state (based on other fields of state and props)?

Ugly way to do it:

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

In this case I get infinite loop of updates or in the best case (if I use PureComponent) double rendering invocation.

The best solution I found so far (but still ugly): Is to create a calculated object in state, which contains calculated fields and updated in componentWillUpdate avoiding 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>

This is also ugly solution and React does not recommended to mutate state avoiding setState. So what is right solution for that?

NOTE:

In my real application I cannot recalculate f(a,b) every single time during rendering, because it's actually complex object, so I need to cache it somehow and the best way is in the state.

6条回答
Fickle 薄情
2楼-- · 2020-07-09 10:50

It looks like the "state" is the place to store everything (even computed values) you'll need to use on the render function, but usually we have the problem you describe.

Since React 16.3 a new approach for this situation has been given in the way of the static getDerivedStateFromProps (nextProps, prevState) "lifecycle hook".

You should update at least to this version if you haven't, and follow the advice given by the React Team on their blog.

Here is the official documentation for this functionality.

The issue here is that this function is invoked before every render, and being "static" you cannot access the current instance previous props, which is usually needed to decide if the computed value must be generated again or not (I suppose this is your case, as you have stated your computation process is heavy). In this case, the React team suggests to store in the state the values of the related props, so they can be compared with the new ones:

if (nextProps.propToCompute !== prevState.propToComputePrevValue) {
  return {
    computedValue: Compute(nextProp.propToCompute),
    propToComputePrevValue: nextProps.propToCompute
  };
}
return null;
查看更多
ゆ 、 Hurt°
3楼-- · 2020-07-09 10:51

Do not include redundant information in your state.

A simplified example is having firstName and lastName in your state. If we want to display the full name in your render method, you would simply do:

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

}

I like this example because it's easy to see that adding a fullName in our state, that just holds ${this.state.firstName} ${this.state.lastName} is unnecessary. We do string concatenation every time our component renders, and we're okay with that because it's a cheap operation.

In your example, your calculation is cheap so you should do it in the render method as well.

查看更多
beautiful°
4楼-- · 2020-07-09 11:05

If you are using React 16.8.0 and above, you can use React hooks API. I think it's useMemo() hook you might need. For example:

import React, { useMemo } from 'react'

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

  return (
    <div>
      { calculatedValue }
    </div>
  )
}

For more details, refer to the React documentation

查看更多
做个烂人
5楼-- · 2020-07-09 11:08

You're first attempt is the right way to solve this problem. However, you need to add a check to see if state has actually changed:

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
}

You need to check the pieces of state and props that you use in your calculate method and make sure they have changed before updating state again. This will prevent the infinite loop.

查看更多
做自己的国王
6楼-- · 2020-07-09 11:14

I wouldn't advice you to store your calculated value inside your state. My approach would be more like this:

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

The problem with storing the calculation inside your state is, that the calculation can be mutated by multiple sources. If you use my solution, there is no way, that anything can overwrite the calculation WITHOUT using the correct function to calculate them.

查看更多
兄弟一词,经得起流年.
7楼-- · 2020-07-09 11:17

image from question

You can save calculated result in this.calculated instead of this.state. It is dependent data. All data which causes update and render is already in state and 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}
       />
     }
  }
}
查看更多
登录 后发表回答