简体   繁体   中英

React child components don't re-render when mapped from an array

I'm loading some react components on demand (among with other information) depending on user input.
The components to render are kept in an array and the render method uses array.map to include the components.

The problem is, that if I trigger a forceUpdate() of the main app component, the mapped components won't update.

Code example: https://codesandbox.io/s/react-components-map-from-array-ekfb7

In order to update in React, you have to put your data in the state and then setState .

setState() schedules an update to a component's state object. When state changes, the component responds by re-rendering which means updating the screen with the new state.

import React from "react";
import "./styles.css";

class TestComponent extends React.Component {
  render() {
    return <p>{Date.now()}</p>;
  }
}

export class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      comps: [<TestComponent />],
    }
  }

  add = () => {
    this.setState({ comps: this.state.comps.concat(<TestComponent />) })
  }

  render() {
    return (
      <div className="App">
        <h1>Components map example</h1>
        <p></p>
        <h2>Static TestComponent (ok):</h2>
        <TestComponent />

        <h2>TestComponents mapped from an array (not ok):</h2>
        {
          this.state.comps.map((comp, id) => {
            return <div key={id}>{comp}</div>;
          })
        }

        <h2>All should update when the App component renders</h2>
        <p>
          <button onClick={this.add}>Add TestComponent</button>
        </p>
      </div>
    );
  }
}

The dates are not updating because you are creating the instance of the component in your add function, and from then on you are referencing that instance without letting react manage the updates.

This is why storing component instances in state or in other variables is an anti-pattern.

Demonstration of the problem

Below I've created a working example still using forceUpdate just to prove what the issue is.

Notice instead of putting the component in state, I'm just pushing to the array to increase it's length. Then React can manage the updates correctly.

 class TestComponent extends React.Component { render() { return <p>{Date.now()}</p>; } } class App extends React.Component { constructor(props) { super(props); this.comps = [1]; } add() { this.comps.push(1); this.forceUpdate(); } render() { return ( <div className="App"> <h1>Components map example</h1> <p></p> <h2>Static TestComponent (ok):</h2> <TestComponent /> <h2>TestComponents mapped from an array (not ok):</h2> {this.comps.map((comp, id) => { return <div key={id}><TestComponent /></div>; })} <h2>All should update when the App component renders</h2> <p> <button onClick={() => this.add()}>Add TestComponent</button> <button onClick={() => this.forceUpdate()}>forceUpdate App</button> </p> </div> ); } } ReactDOM.render(<App/>,document.getElementById('root'))
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <div id="root"></div>
This is still a less than ideal solution. But it does show where the issue lies.

A better solution

If you need to know more about each component instance up front, you can make the array more complex.

I would also suggest using state to store the comps array, and removing forceUpdate completely.

 class TestComponent extends React.Component { render() { return <p>{Date.now()} {this.props.a} {this.props.b}</p>; } } class App extends React.Component { constructor(props) { super(props); this.state = { comps: [{ a: 'a', b: 'b' }] } } add = () => { // add your custom props here this.setState(prev => ({comps: [...prev.comps, { a: 'c', b: 'd' } ]})); } render() { return ( <div className="App"> <h1>Components map example</h1> <p></p> <h2>Static TestComponent (ok):</h2> <TestComponent /> <h2>TestComponents mapped from an array (not ok):</h2> {this.state.comps.map((compProps, id) => { return <div key={id}><TestComponent {...compProps} /></div>; })} <h2>All should update when the App component renders</h2> <p> <button onClick={() => this.add()}>Add TestComponent</button> </p> </div> ); } } ReactDOM.render(<App/>,document.getElementById('root'))
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <div id="root"></div>

Now notice that each component in the map callback can have it's own unique set of props based on whatever logic you what. But the parts that should re-render will do so correctly.

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