Search code examples
javascriptreactjsreact-reduxdeprecatedcomponentwillreceiveprops

React component lifecycle componentWillReceiveProps deprecation causes problem with Redux


I have 2 components, one that is used to add new post to an array of posts and another component that maps through that array of posts and lists them on the page.

I want to add the new post to the top of the array with unshift(), but ever since componentWillReceiveProps was deprecated, I've been struggling to find a solution with the new getDerivedStateFromProps method.


Here is Postform.js:

import React, { Component } from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { createPost } from "../actions/postActions";

class PostForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      title: "",
      body: "",
    };

    this.onChange = this.onChange.bind(this); // Bind onChange event
    this.onSubmit = this.onSubmit.bind(this); // Bind onSubmit event
  }

  // Set state when changing input value
  onChange(e) {
    this.setState({ [e.target.name]: e.target.value });
  }

  onSubmit(e) {
    e.preventDefault();

    const post = {
      title: this.state.title,
      body: this.state.body,
    };

    this.props.createPost(post);
  }

  render() {
    return (
      <div>
        <h1>Add Post</h1>
        <form onSubmit={this.onSubmit}>
          <div>
            <label>Title: </label>
            <br />
            <input
              type="text"
              name="title"
              onChange={this.onChange}
              value={this.state.title}
            />
          </div>
          <br />
          <div>
            <label>Body: </label>
            <br />
            <textarea
              name="body"
              onChange={this.onChange}
              value={this.state.body}
            />
          </div>
          <br />
          <button type="submit">Submit</button>
        </form>
      </div>
    );
  }
}

PostForm.propTypes = {
  createPost: PropTypes.func.isRequired,
};

export default connect(null, { createPost })(PostForm);

Here is Posts.js:

import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { fetchPosts } from "../actions/postActions";

class Posts extends Component {
  state = {
    newPost: this.props.newPost,
  };

  componentDidMount() {
    this.props.fetchPosts();
  }

  // The new way I am struggling with
  static getDerivedStateFromProps(props, state) {
    if (props.newPost !== state.newPost) {
      props.posts.unshift(props.newPost);
    }
    return null;
  }

   // The old way, now renamed with UNSAFE_ prefix
  /* UNSAFE_componentWillReceiveProps(nextProps) {
    if (nextProps.newPost) {
      this.props.posts.unshift(nextProps.newPost);
    }
  } */

  render() {
    const postItems = this.props.posts.map((post) => (
      <div key={post.id + Math.random()}>
        <h3>{post.title}</h3>
        <p>{post.body}</p>
      </div>
    ));

    return (
      <div>
        <h1>Posts</h1>
        {postItems}
      </div>
    );
  }
}

Posts.propTypes = {
  fetchPosts: PropTypes.func.isRequired,
  posts: PropTypes.array.isRequired,
  newPost: PropTypes.object,
};

const mapStateToProps = (state) => ({
  posts: state.posts.items,
  newPost: state.posts.item,
});

export default connect(mapStateToProps, { fetchPosts })(Posts);


The problem is that getDerivedStateFromProps is called twice (initial mount and every render),

unlike componentWillReceiveProps which is called only once.

This causes the newPost to be added twice to the array and thus it shows double on the page too.


I am using Redux here, so the state should be in the store (mostly is), but I've added a state for the Posts.js component since the getDerivedStateFromProps won't otherwise work (as far as I've tried).


In a nutshell: How do I add the newPost to the props-array inside the getDerivedStateFromProps (or other lifecycle method) without getting double results?


Solution

  • 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 as per react docs : https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops

    You already seem to be using redux, thus once you have done createPost why don't you do array.unshift while updating your posts in the Redux Store and thus your store will hold the state.posts.items with your latest post on top.