Nikolay Aleshkovskiy Nikolay Aleshkovskiy - 3 months ago 11
React JSX Question

How to add new elements without re-rendering whole list with React

I'm working on simple stream app. I have list of posts and this list can receive updates, which will display on top of it.

The problem is on each new post receive React rerenders the whole list of elements. I've made simple example for it.

Is there any way to avoid this behaviour?
I've seen the dynamic-children topic on React docs, but in example, as you see, I have all children updated anyway.



class Post extends React.Component {
render() {
console.log('rerendered post', this.props.reactKey);
return (
<li>{this.props.post.text}</li>
);
}
}

class App extends React.Component {

constructor(props) {
super(props);
this.state = {posts: [
{id: '00001', text: 'First one'},
{id: '00002',text: 'Second one'},
{id: '00003',text: 'Third one'}
]};
}

addPost() {
const posts = this.state.posts;
posts.unshift({id: '00004', text: 'New post'});
this.setState({posts: posts});
}

render() {
return (
<div>
<button onClick={this.addPost.bind(this)}>Add Post</button>
<ul>
{this.state.posts.map((post, index) => {
return (<Post post={post} key={post.id} reactKey={index} />);
})}
</ul>
</div>
);
}
}


ReactDOM.render(<App />, document.getElementById('root'));

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
<div id="root"></div>
</body>





The solution

The problem was that I used index of .map function is a key for each list component instead of unique key. And because after adding new element to list all indexes changes to +1, so the first post becomes the second, all my posts have re-rendered.
So at first, check you use unique keys across all list elements :-)

Answer

Work that needs to be done only once should be done in a lifecycle method that is guaranteed to run only once, like componentDidMount. As the docs suggest:

If you want to integrate with other JavaScript frameworks, set timers using setTimeout or setInterval, or send AJAX requests, perform those operations in this method.

I added logging to componentDidMount in your snippet to show rendering happens many times, but componentDidMount is called only once per instance.

class Post extends React.Component {
  componentDidMount() {
    console.log('mounted post', this.props.id);
  }

  render() {
    console.log('rerendered post', this.props.id);
    return (
      <li>{this.props.post.text}</li>
    );
  }
}

class App extends React.Component {

  constructor(props) {
    super(props);
    this.nextId = 4;
    this.state = {
      posts: [
        {id: 1, text: 'First one'},
        {id: 2,text: 'Second one'},
        {id: 3,text: 'Third one'},
      ],
    };
  }

  addPost() {
    const posts = this.state.posts;
    posts.unshift({id: this.nextId, text: 'Post ' + this.nextId});
    this.nextId++;
    this.setState({posts: posts});
  }

  render() {
    return (
      <div>
        <button onClick={this.addPost.bind(this)}>Add Post</button>
        <ul>
          {this.state.posts.map((post, index) => {
            return (<Post post={post} key={post.id} id={post.id} />);
          })}
        </ul>
      </div>
    );
  }
}


ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<body>
  <div id="root"></div>
</body>