React list key context


#1

I’m working through the Lists and Keys section of the React tutorial.


I’ve created a component, Board (code below), which draws a tic-tac-toe board made up of buttons contained in Square (code omitted) components. The Square components are items in row arrays which are themselves items in a board array to form a grid made up of nested arrays.

I have a question about the following paragraph from the tutorial.

Extracting Components with Keys
Keys only make sense in the context of the surrounding array. For example, if you extract a ListItem component, you should keep the key on the <ListItem /> elements in the array rather than on the <li> element in the ListItem itself.

In my code below, I have a method, renderSquare, which returns individual elements in the form of Square components. These elements are stored in an array, so they need a key prop for React to work its rendering magic. Because renderSquare is an unbound old-style function/method, I think it takes on the execution context of gridBuilder, so there’s no problem with assigning the key within the renderSquare method. However, I’m not sure if using an arrow function for renderSquare would break the key rule I quoted above.

Can anyone tell me if I’m understanding the tutorial correctly in regards to the key prop? Do list-item factories like renderSquare break how array keys work in React? Does the key absolutely need to be assigned where the list is being created, or are there exceptions?

class Board extends React.Component {
    renderSquare(i) {
        return (
            <Square
                key={"square"+i}
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
            />
        );
    }
    
    // replaced hard-coded grid with loop
    gridBuilder() {
        let gridElems = [];
        let rowElems = [];
        for (let i = 0; i < 9; i++) {
            // is it a problem if renderSquare key assignment
            // doesn't happen in the gridBuilder context?
            let elem = (this.renderSquare(i));
            rowElems.push(elem);
            if ((i + 1) % 3 === 0) {
                let row = (
                    <div className="board-row" key={"row" + ((i+1)/3)}>
                        {rowElems}
                    </div>);
                gridElems.push(row);
                rowElems = [];
            }
        }

        return <div>{gridElems}</div>
    }
    render () {
        return (
            this.gridBuilder()
        );
    }
}

#2

No idea, that paragraph is total gibberish. Look at the sample code more as I think they’ve written a paragraph that makes no sense but the code should be right.


#3

Ok, the code was right after all, but I decided to dig a little deeper.

It seems as though React can take an array filled with components and render those components as siblings inside a list (like a standard html list). Because list items can be added and removed, (eg. in a shopping cart), React needs some kind of key to identify each item in the list. Here’s what the JSX would look like if you included a component array.

let giftsForMom = [ "lambo", "ferrari", "maserati" ];
// convert the Array of strings into an Array of list-item components
let momShopList = this.makeShoppingList(giftsForMom);
return (
  <ul>
      {momShopList}
  </ul>
);

Supposedly, React does this automatically if you don’t provide keys, but you’ll get a console warning that keys weren’t provided. The documentation is confusing, so I could be wrong about the automatic key generation. In any case, best practice is to create unique keys for each item in the list. Most of the time, if the items were generated from a loop, the loop index number converted to a string would suffice. eg. indexNumber.toString().

There’s a problem with this approach in the following example.

let giftsForMom = [ "lambo", "ferrari", "maserati" ];
let momShopList = this.makeShoppingList(giftsForMom);
let giftsForDad = [ "spats", "platforms", "slippers" ]; 
let dadShopList = this.makeShoppingList(giftsForDad);
return (
  <ul>
      {momShopList}
      {dadShopList}
  </ul>
);

Because we’re iterating over two Arrays, the keys will be the same for corresponding items in momShopList and dadShopList. I read a solution that was to centralize key generation in a global function. I took it a step further and made it an object with a key-generator method which increments the index property of the object.
It looks like this.

let keyMaker = {
    index : 0,
    generate : function() {
        let retVal = this.index;
        this.index += 1;
        return retVal.toString()
    }
}

I tried it out and it worked. Here’s a screenshot of a demo using a produce basket of fruits and veggies.


If you look on the far right pane, the props of ProduceBasket include two Arrays. Each of them has four elements with corresponding index numbers. Now if you look at Elements pane, you’ll see that each key is a unique number converted to a string. It works!

In practice, there’s nothing to stop anyone from having duplicate keys in a list, but the official wisdom is that this could potentially reduce React’s rendering performance.

Good to know!
Here’s the source for the produce basket demo.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'

// This is a demonstration of how to avoid duplicate keys in React
let fruitNames = ['dragonfruit', 'persimmon', 'kiwi', 'durian'];
let veggieNames = ['chouchou', 'bittermelon', 'nappa', 'rapini'];

// The list item Component is not stored directly in the produceItems array.
// Instead, the list item is wrapped in a ProduceItem Component which stored 
// in the array. That means React needs a unique list key assigned to each
// ProduceItem.
let ProduceItem = (props) => {
    return (
        <li>{props.name}</li>
    );
}

// If I have two Arrays being converted to list-items, I need a
// way to generate unique keys that don't depend on Array indexes
// because the indexes will be duplicated. e.g. 'dragonfruit' and
// 'chouchou' have the same index and would receive identical
// keys. keyMaker maintains a master index counter for all list-items
// to ensure uniqueness.

let keyMaker = {
    index : 0,
    generate : function() {
        let retVal = this.index;
        this.index += 1;
        return retVal.toString()
    }
}

class ProduceBasket extends React.Component {
    constructor(props) {
        super(props);
        // convert array of strings to array of ProduceItems
        this.fruits = this.prepFood(props.fruitNames);
        this.veggies = this.prepFood(props.veggieNames);
    }

    prepFood = (names) => {
        let produceItems = [];
        for (let i = 0; i < names.length; i++) {
            // don't use the loop index for item keys in case more than one array is
            // being rendered into the item list. When invoked from JSX ProduceItem
            // automatically assigns the props.key to a property in its constructor. 
            // This can be done explicitly in class Component constructors.
            produceItems.push(<ProduceItem key={keyMaker.generate()} name={names[i]} />);
        }
        return produceItems
    }

    render() {
        return (
            <ul>
                {/* These two Arrays get added to the same item list. There
                    is no intermediate wrapper or element between the items
                    and the unordered list component.*/}
                {this.fruits}
                {this.veggies}
            </ul>
        );
    }
}

ReactDOM.render(
    <ProduceBasket fruitNames={fruitNames} veggieNames={veggieNames} />,
    document.getElementById('root')
);```