Search code examples
javascriptarraysreactjsreact-component

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


Solution

  • 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.