Why getDerivedStateFromProps does not allow to re-render with the state update? Not a problem for componentWillReceiveProps - ReactJS

Viewed 666

I'm trying to move from componentWillReceiveProps to getDerivedStateFromProps and in some cases, I was successful but when the case is append the props to the existing state, then things start to not behaving the same way. When a make an update to the component's state, the state changes (and the component did after updated) but still renders the previous state. Something weird happens when using getDerivedStateFromProp instead of componentWillReceiveProps. It seems that method does not handle well with 'internal' changes. In the following example, I have getDerivedStateFromProp on Child and it works, but because is only rendering the props. This behavior was also observed in a simpler example where I didn't have any child components and was just rendering state changes.

The code below shows a child component that is used to print/show the data received by props while uses a delete data handler (to remove data that is stored at Parent from child component interaction). When using getDerivedStateFromProps() I can't access to this.state and the prevState doesn't mean the same since the state is accumulative. And when I remove data from the child component, doesn't update the props of the child (while using componentWillReceiveProps was OK). So, I do not find a way to substitute my UNSAFE_componentWillReceiveProps

componentWillReceiveProps:


  UNSAFE_componentWillReceiveProps(nextProps){
    this.setState({
      data: [...this.state.data,...nextProps.data]
    })
  }

getDerivedStateFromProps:

   static getDerivedStateFromProps(nextProps,state) {

    if (!isEqual(nextProps.data, state.data)) {
      return {
        data: [...state.data, ...nextProps.data]
      };
    }
    return null;
  }


The original code that works as intended (before getDerivedStateFromProps on Parent Comp.)

DataConsole - Parent Component:

export class DataConsole extends Component {
  // Used for unsubscribing when our components unmount
  unsub = null;

  static defaultProps = {
    data: [],
  };

  constructor(props) {
    super(props);
    this.state = {
      data: [],
    };

    this.handleTableRowClick = this.handleTableRowClick.bind(this);
  }


   UNSAFE_componentWillReceiveProps(nextProps){
    this.setState({
      data: [...this.state.data,...nextProps.data]
    })
  }

  handleTableRowClick(key) {
    console.log(
      "handleTable",
      key,
      this.state.data[key],
      this.state.data.length
    );
     const e = this.state.data.splice(key, 1)
     //console.log("remove?", e , this.state.data.length)

    this.setState({
      undoDataRemove: e
    });
  }


  render() {
    return (
      <div>
        <Container
          fluid
          style={{ paddingLeft: 0, paddingRight: 0 }}
          className="DataContainer"
        >
          <Row noGutters>
            <Col sm={8} className="ConsoleTable">
              <div>
                <DataViewer
                  data={this.state.data}
                  rowClickHandler={this.handleTableRowClick}
                />
              </div>

       ...




DataViewer - Child Component

import isEqual from "react-fast-compare";
...

export class DataViewer extends Component {
  static defaultProps = {
    data: [],
  };

  constructor(props){
    super(props)
    this.state={data: []}
  }


  componentDidUpdate() {
    console.log("DataViewer updated");
  }

  static getDerivedStateFromProps(nextProps, prevProps) {

    console.log(nextProps, prevProps)
    if (!isEqual(nextProps.data, prevProps.data)) {
      return {
        data: nextProps.data
      };
    }
    return null;
  }


  render() {
    return (
      <div className={"TableData"}>
        <Table responsive="lg" striped borderless hover>
          <tbody>
            {this.state.data.map((elem, ids) => {
              if (!isEmpty(elem)) {
                return (
                  <tr key={ids} onClick={() => this.props.rowClickHandler(ids)}>
                    <td>{ids + 1}</td>
                    {Object.keys(elem).map(function (value, idx) {
                      return (
                        <td key={idx}>
                          {value}:{elem[value]}
                        </td>
                      );
                    })}
                  </tr>
                );
              } else {
                return null;
              }
            })}
          </tbody>
        </Table>
      </div>
    );
  }
}


1 Answers

There is a bug in your code that causes your problem, and it is unrelated to getDerivedStateFromProps and UNSAFE_componentWillReceiveProps.

The faulty line is this:

const e = this.state.data.splice(key, 1) 

It changes this.state.data without calling setState. Never do that. The only way you are ever allowed to make any changes to this.state is via this.setState or by returning something to be merged into state from getDerivedStateFromProps, not any other way.

In order to more easily cope with changes deep in your state tree, immutable libraries come in handily. immer is currently among the top candidates in this category. Using immer, you can modify state any way you want, as long as you wrap it into a produce call using the pattern this.setState(produce(this.state, newState => { /* change newState here */ })):

import produce from 'immer';

// ...

this.setState(produce(this.state, newState => {
  const e = newState.data.splice(key, 1);
  newState.undoDataRemove = e;
}));

Hope, this can help.

Related