简体   繁体   中英

How to not use setState inside render function in React

I have a complete running code, but it have a flaw. It is calling setState() from inside a render(). So, react throws the anti-pattern warning.

Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount

My logic is like this. In index.js parent component, i have code as below. The constructor() calls the graphs() with initial value, to display a graph. The user also have a form to specify the new value and submit the form. It runs the graphs() again with the new value and re-renders the graph.

import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';

const initialval = '8998998998';

class Dist extends Component {
  constructor() {
    this.state = {
      checkData: true,
      theData: ''
    };
    this.graphs(initialval);
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.cost !== nextProps.cost) {
      this.setState({
        checkData: true
      });
    }
  }

  graphs(val) {
    //Calls a redux action creator and goes through the redux process
    this.props.init(val);
  }

  render() {
    if (this.props.cost.length && this.state.checkData) {
      const tmp = this.props.cost;
      //some calculations
      ....
      ....
      this.setState({
        theData: tmp,
        checkData: false
      });
    }

    return (
      <div>
        <FormComponent onGpChange={recData => this.graphs(recData)} />
        <PieGraph theData={this.state.theData} />
      </div>
    );
  }
}

The FormComponent is an ordinary form with input field and a submit button like below. It sends the callback function to the Parent component, which triggers the graphs() and also componentWillReceiveProps.

handleFormSubmit = (e) => {
    this.props.onGpChange(this.state.value);
    e.preventdefaults();
}

The code is all working fine. Is there a better way to do it ? Without doing setState in render() ?

Never do setState in render. The reason you are not supposed to do that because for every setState your component will re render so doing setState in render will lead to infinite loop, which is not recommended.

checkData boolean variable is not needed. You can directly compare previous cost and current cost in componentWillReceiveProps, if they are not equal then assign cost to theData using setState. Refer below updated solution.

Also start using shouldComponentUpdate menthod in all statefull components to avoid unnecessary re-renderings. This is one best pratice and recommended method in every statefull component.

import React, { Component } from 'react';
import FormComponent from './FormComponent';
import PieGraph from './PieGraph';

const initialval = '8998998998';

class Dist extends Component {
  constructor() {
    this.state = {
      theData: ''
    };
    this.graphs(initialval);
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.cost != nextProps.cost) {
      this.setState({
        theData: this.props.cost
      });
    }
  }

  shouldComponentUpdate(nextProps, nextState){
     if(nextProps.cost !== this.props.cost){
         return true;
     }
     return false;
  }
  graphs(val) {
    //Calls a redux action creator and goes through the redux process
    this.props.init(val);
  }

  render() {
    return (
      <div>
        <FormComponent onGpChange={recData => this.graphs(recData)} />
        {this.state.theData !== "" && <PieGraph theData={this.state.theData} />}
      </div>
    );
  }
}

PS:- The above solution is for version React v15.

You should not use componentWillReceiveProps because in most recent versions it's UNSAFE and it won't work well with async rendering coming for React.

There are other ways!

static getDerivedStateFromProps(props, state)

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.

So in your case

...component code
static getDerivedStateFromProps(props,state) {
  if (this.props.cost == nextProps.cost) {
    // null means no update to state
    return null;
  }

  // return object to update the state
  return { theData: this.props.cost };
}
... rest of code

You can also use memoization but in your case it's up to you to decide. The link has one example where you can achieve the same result with memoization and getDerivedStateFromProps

For example updating a list (searching) after a prop changed You could go from this:

static getDerivedStateFromProps(props, state) {
    // Re-run the filter whenever the list array or filter text change.
    // Note we need to store prevPropsList and prevFilterText to detect changes.
    if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    ) {
      return {
        prevPropsList: props.list,
        prevFilterText: state.filterText,
        filteredList: props.list.filter(item => item.text.includes(state.filterText))
      };
    }
    return null;
  }

to this:

import memoize from "memoize-one";

class Example extends Component {
  // State only needs to hold the current filter text value:
  state = { filterText: "" };

  // Re-run the filter whenever the list array or filter text changes:
  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

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

  render() {
    // Calculate the latest filtered list. If these arguments haven't changed
    // since the last render, `memoize-one` will reuse the last return value.
    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>
    );
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM