Course: Section

Get Started with Modern React: Learn by Doing

Episode: Title

S03・V19: Key Prop

Date Created: July 17th, 2019
Last Updated: 27 days ago

Objective
  • We will explain why React requires that elements in a list require a unique key.
Watch Video
Duration: 5m 6s

Unique Key Prop

Let’s have a look at the browser’s console. Note that there is a “Warning: Each child in a list should have a unique ‘key’ prop.” We will explain why this warning is showing. When we render a list, React stores some information about each rendered list item. When we update a list, React needs to determine what has changed. We could have added, removed, re-arranged, or updated the list’s items.

Let’s say that initially we have a list of two items.

/** Unique Keys
 * 
 * Before:
 * <li>Alexa: 7 tasks left</li>
 * <li>Ben: 5 tasks left</li>
 */

After a transition, we have the following list of three items.

/** Unique Keys
 * 
 * Before:
 * <li>Alexa: 7 tasks left</li>
 * <li>Ben: 5 tasks left</li>
 * 
 * After: * <li>Ben: 9 tasks left</li> * <li>Claudia: 8 tasks left</li> * <li>Alexa: 5 tasks left</li> */

In addition to the updated counts, a human reading this would probably say that we swapped Alexa and Ben’s ordering, and inserted Claudia between Alexa and Ben.

However, React is a computer program and does not know what we intended.

Because React cannot know our intentions, we need to specify a key property for each list item to differentiate each list item from its siblings. If we were displaying data from a database, Alexa, Ben, and Claudia’s database IDs could be used as keys.

/** Unique Keys
 * 
 * Before:
 * <li>Alexa: 7 tasks left</li>
 * <li>Ben: 5 tasks left</li>
 * 
 * After:
 * <li>Ben: 9 tasks left</li>
 * <li>Claudia: 8 tasks left</li>
 * <li>Alexa: 5 tasks left</li>
 *
 * Add key: * <li key={user.id}>{user.name}: {user.taskCount} tasks left</li> */

When a list is re-rendered, React takes each list item’s key and searches the previous list’s items for a matching key.

  • If the current list has a key that didn’t exist before, React creates a component.
  • If the current list is missing a key that existed in the previous list, React destroys the previous component.
  • If two keys match, the corresponding component is moved.

Keys tell React about the identity of each component which allows React to maintain state between re-renders. If a component’s key changes, the component will be destroyed and re-created with a new state.

Specifics about ‘key’

key is a special and reserved prop in React. When an element is created, React extracts the key property and stores the key directly on the returned element.

Even though key may look like it belongs in props, key cannot be referenced from the props object. In other words, you cannot reference key using props.key in a child component.

React automatically uses key to decide which components to update. A component cannot inquire about its key.

It is strongly recommended that you assign proper keys whenever you build dynamic lists. If you don’t have an appropriate key, you may want to consider restructuring your data so that you do. If no key is specified, React will present a warning and use the array index as a key by default. Using the array index as a key is problematic when trying to re-order a list’s items or inserting or removing list items.

keys do not need to be globally unique. keys only need to be unique between components and their siblings.

Adding a ‘key’ to the Game

In our game’s history, each past move has a unique ID associated with it. The unique ID is the sequential number (index) of the move. The moves are never re-ordered, deleted, or inserted in the middle, so it’s safe to use the move index as a key.

  const moves = history.map((move, index) => {
    const description = Boolean(index)
      ? `Go to move #${index}`
      : `Go to game start`;
    return (
      <li key={index}>        <button>
          {description}
        </button>
      </li>
    );
  });

View the browser console. The warning has gone.

Code Snapshot

src/index.js
import React, { useState } from 'react';
import ReactDOM from 'react-dom';

import './index.css';

const Square = props => {
  return (
    <button
      className="square"
      onClick={props.onClickEvent}
    >
      {props.value}
    </button>
  );
};

const Board = props => {
  const renderSquare = i => {
    return (
      <Square
        value={squares[i]}
        onClickEvent={() => handleClickEvent(i)}
      />
    );
  };

  return (
    <div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  );
};

const Game = () => {
  const initialHistory = [
    { squares: Array(9).fill(null) },
  ];
  const [history, setHistory] = useState(initialHistory);
  const [xIsNext, setXIsNext] = useState(true);

  const handleClickEvent = i => {
    const currentMove = history[history.length - 1];
    const newSquares = [...currentMove.squares];

    // Return early if winner declared or square filled
    const winnerDeclared = Boolean(calculateWinner(newSquares));
    const squareFilled = Boolean(newSquares[i]);
    if (winnerDeclared || squareFilled) {
      return;
    }

    newSquares[i] = xIsNext ? 'X' : 'O';
    const newMove = { squares: newSquares };
    const newHistory = [...history, newMove];
    setHistory(newHistory);
    setXIsNext(!xIsNext);
  };

  const moves = history.map((move, index) => {
    const description = Boolean(index)
      ? `Go to move #${index}`
      : `Go to game start`;
    return (
      <li key={index}>
        <button>
          {description}
        </button>
      </li>
    );
  });

  const currentMove = history[history.length - 1];
  const winner = calculateWinner(currentMove.squares);
  const status = winner ?
    `Winner: ${winner}` :
    `Next player: ${xIsNext ? 'X' : 'O'}`;

  return (
    <div className="game">
      <Board 
        squares={currentMove.squares}
        onClickEvent={i => handleClickEvent(i)}
      />
      <div className="status">{status}</div>
      <ol>{moves}</ol>
    </div>
  );
};

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

function calculateWinner(squares) {
  /*
  0 1 2
  3 4 5
  6 7 8
  */
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  for (let line of lines) {
    const [a, b, c] = line;
    if (
      squares[a] &&
      squares[a] === squares[b] &&
      squares[a] === squares[c]
    ) {
      return squares[a];
    }
  }

  return null;
}

Summary

In this video we explained how to use the key prop in React, and why it is important.

Next Up…

In the next video, we will return to the game’s “time travel” feature, completing it with the ability to jump to any move in the game’s history, and continue from that point with new moves.