0

Inside this codepen there is React application that renders list of ToDo's. It uses .map() index parameter as key values to render this list. And yes - I know that this is the source of this behaviour, so Please don't tell this in answers.

I've added few items there after initial load and then I see this:

enter image description here

Then I sort it by date and type something into first item

enter image description here

Then I click on Sort by Latest and get this:

enter image description here

Question: Why React fails to render changes in array of JSX element when using .map index as a key in this particular example?

P.S. Here is the code for convenience:

const ToDo = props => (
  <tr>
    <td>
      <label>{props.id}</label>
    </td>
    <td>
      <input />
    </td>
    <td>
      <label>{props.createdAt.toTimeString()}</label>
    </td>
  </tr>
);

class ToDoList extends React.Component {
  constructor() {
    super();
    const date = new Date();
    const todoCounter = 1;
    this.state = {
      todoCounter: todoCounter,
      list: [
        {
          id: todoCounter,
          createdAt: date,
        },
      ],
    };
  }

  sortByEarliest() {
    const sortedList = this.state.list.sort((a, b) => {
      return a.createdAt - b.createdAt;
    });
    this.setState({
      list: [...sortedList],
    });
  }

  sortByLatest() {
    const sortedList = this.state.list.sort((a, b) => {
      return b.createdAt - a.createdAt;
    });
    this.setState({
      list: [...sortedList],
    });
  }

  addToEnd() {
    const date = new Date();
    const nextId = this.state.todoCounter + 1;
    const newList = [
      ...this.state.list,
      {id: nextId, createdAt: date},
    ];
    this.setState({
      list: newList,
      todoCounter: nextId,
    });
  }

  addToStart() {
    const date = new Date();
    const nextId = this.state.todoCounter + 1;
    const newList = [
      {id: nextId, createdAt: date},
      ...this.state.list,
    ];
    this.setState({
      list: newList,
      todoCounter: nextId,
    });
  }

  render() {
    return (
      <div>
        <code>key=index</code>
        <br />
        <button onClick={this.addToStart.bind(this)}>
          Add New to Start
        </button>
        <button onClick={this.addToEnd.bind(this)}>
          Add New to End
        </button>
        <button onClick={this.sortByEarliest.bind(this)}>
          Sort by Earliest
        </button>
        <button onClick={this.sortByLatest.bind(this)}>
          Sort by Latest
        </button>
        <table>
          <tr>
            <th>ID</th>
            <th />
            <th>created at</th>
          </tr>
          {this.state.list.map((todo, index) => (
            <ToDo key={index} {...todo} />
          ))}
        </table>
      </div>
    );
  }
}

ReactDOM.render(
  <ToDoList />,
  document.getElementById('root')
);
zmii
  • 4,123
  • 3
  • 40
  • 64

3 Answers3

3

It seems that inputs are not changed at all here and as they are not controlled (so their attributes/text values didn't change), it makes sense as the root element is not changed in context of React reconciliation (it has still same key). Then React goes to text nodes that eventually are different and then refreshes (updates DOM) only those changed nodes.

So there are two variants to make this work:

  1. Use meaningful key, not index from .map()
  2. Add (in our specific case) attributes to <input />

In general first approach is better as the key property will invalidate the root element (in our case whole ToDo) and force it to render itself, saving us comparison of nested subtree.

zmii
  • 4,123
  • 3
  • 40
  • 64
0

Question: Why React fails to render changes in array of JSX element when using .map index as a key in this particular example?

Don't use the list index as a key. The point of the key is to identify your individual entries from one render to another. When you change the order of the array, react will not know that anything has changed, because the order of the indexes in the todo array is still 0, 1, 2, 3 ... Instead use the todo.id as key.

React hasn't failed to render the new order. It's doing exactly as expected. You are telling it the order is exactly the same as last render, but that the properties of the individual todo items have changed.

What you want to say is that it's the ordering that has changed. Then you have to pass a meaningful key. Then react will use the same dom elements, but change the order.

This means that you can do something like react-shuffle where a transition effect lets you see how the elements are reordered.

enter image description here

Håken Lid
  • 22,318
  • 9
  • 52
  • 67
  • I know this, but want to understand why is this so. Please tell if this makes sense: https://stackoverflow.com/a/48989970/2730688 ? – zmii Feb 26 '18 at 14:03
  • 1
    Check this question. It's not a duplicate of this one, but the answers contain an explanation of how `key` works, and what it's for. https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js – Håken Lid Feb 26 '18 at 14:05
0

This is because you do not have stable ID's (your indexes change after the filter). See the docs here.

And you have another problem, because you are mutating your list when you sort it:

sortByEarliest() {
    const sortedList = this.state.list.sort((a, b) => {
      return a.createdAt - b.createdAt;
    });
    this.setState({
      list: [...sortedList],
    });
  }

  sortByLatest() {
    const sortedList = this.state.list.sort((a, b) => {
      return b.createdAt - a.createdAt;
    });
    this.setState({
      list: [...sortedList],
    });
  }

The sort alter your array. So you could use this solution:

  sortByEarliest() {
    const { list } = this.state;
    list.sort((a, b) => a.createdAt - b.createdAt);
    this.setState({ list });
  }

  sortByLatest() {
    const { list } = this.state;
    list.sort((a, b) => b.createdAt - a.createdAt);
    this.setState({ list });
  }
Alessander França
  • 2,697
  • 2
  • 29
  • 52
  • your answer doesn't answer my question. I know conditions when this will work as they are in the docs link you've provided and that I've read yesterday. My question is - WHY in this specific situation it behaves like this - with `key={index}`. **Secondly** I didn't get part about sort - making this change doesn't make the application work. so the question persists. – zmii Feb 27 '18 at 12:29