Update object data-members in React state doesn't trigger re-rendering

I'm using TypeScript and React to try to implement my custom types in React component. However the component is not re-rendering if data members of object in state has updated.

Here's my code:

class Person {
  name: string;
  surname: string;

  constructor(name: string, surname: string) {
    this.name = name;
    this.surname = surname;
  }
}

class App extends Component<AppProps, AppState> {
  constructor(props) {
    super(props);
    this.state = {
      persons: [new Person("Alpha", "A"), new Person("Bravo", "B")]
    };
  }

  render() {
    return (
      <div>
        <button
          onClick={() => {
            this.state.persons[0].name = "Zulu";
            this.state.persons[0].surname = "Z";
          }}
        >
          Change A to Z
        </button>

        <button
          onClick={() => {
            this.setState({
              persons: [...this.state.persons, new Person("Charlie", "C")]
            });
          }}
        >
          Add C
        </button>

        <button onClick={() => { this.forceUpdate(); }}> Force Update </button>

        {this.state.persons.map(person => (
          <p>
            {person.name} {person.surname}
          </p>
        ))}
      </div>
    );
  }
}

Full Code Snippet

If I click on Change A to Z nothing will change - unless Add C or Force Update is clicked. I am assuming React cannot detect changes made in Person therefore no re-render.

I have some questions about this problem.

  • Is this approach (custom datatype as state) recommended to use in React
    • If yes - how do I make this work? (React able to detect changes made in Person)
    • If no - what is the recommended approach to use custom datatype in React?

1 answer

  • answered 2021-01-11 05:15 CertainPerformance

    React will not re-render by default if every element in the new state is === to every element in the old state. You also have to call setState in order to trigger a re-render. Your Change A to Z is failing on both counts - you need to both create a new persons array without mutating what's currently in state, and then call setState with the new array.

    Also, having a Person instance doesn't look to be accomplishing anything, since there are no methods on the class - and it'll make changing the state harder, so I'd remove Person entirely. Try

      constructor(props) {
        super(props);
        this.state = {
          persons: [{ name: 'Alpha', surname: 'A' }, { name: 'Bravo', surname: 'B' }]
        };
      }
    

    Then change

          onClick={() => {
            this.state.persons[0].name = "Zulu";
            this.state.persons[0].surname = "Z";
          }}
    

    to

    onClick={() => {
      this.setState({
        persons: [{ name: 'Zulu', surname: 'Z' }, ...this.state.persons.slice(1)]
      });
    }}
    

    The

    persons: [{ name: 'Zulu', surname: 'Z' }, ...this.state.persons.slice(1)]
    

    effectively immutably replaces the first element of the array with the new Zulu object.