ReactJS - Keys



List and keys

We learned how to use collections in React using for loop and map function in previous chapters. If we run the application, it output as expected. If we open the developer console in the browser, then it will show a warning as shown below −

Warning: Each child in a list should have a unique "key" prop.
Check the render method of `ExpenseListUsingForLoop`. See https://reactjs.org/link/warning-keys for more information.
tr
ExpenseListUsingForLoop@
div
App

So, what does it means and how it affects our React application. As we know, React tries to render only the updated values in DOM through various mechanism. When the React renders a collection, it tries to optimize the rendering by updating only the updated item in the list.

But, there is no hint for the React to find which items are new, updated or removed. To get the information, React allows a key attributes for all components. The only requirement is that the value of the key should be unique among the current collection.

Let us recreate one of our earlier application and apply the key attributes.

create-react-app myapp
cd myapp
npm start

Next, create a component, ExpenseListUsingForLoop under components folder (src/components/ExpenseListUsingForLoop.js).

import React from 'react'
class ExpenseListUsingForLoop extends React.Component {
   render() {
      return <table>
         <thead>
            <tr>
               <th>Item</th>
               <th>Amount</th>
            </tr>
         </thead>
         <tbody>
         </tbody>
         <tfoot>
            <tr>
               <th>Sum</th>
               <th></th>
            </tr>
         </tfoot>
      </table>
   }
}
export default ExpenseListUsingForLoop

Here, we created a basic table structure with header and footer. Then, create a function to find the total expense amount. We will use it later in the render method.

getTotalExpenses() {
   var items = this.props['expenses'];
   var total = 0;
   for(let i = 0; i < items.length; i++) {
      total += parseInt(items[i]);
   }
   return total;
}

Here, getTotalExpenses loop over the expense props and summarize the total expenses. Then, add expense items and total amount in the render method.

render() {
   var items = this.props['expenses'];
   var expenses = []
   expenses = items.map((item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item}</td></tr>)
   var total = this.getTotalExpenses();
   return <table>
      <thead>
         <tr>
            <th>Item</th>
            <th>Amount</th>
         </tr>
      </thead>
      <tbody>
         {expenses}
      </tbody>
      <tfoot>
         <tr>
            <th>Sum</th>
            <th>{total}</th>
         </tr>
      </tfoot>
   </table>
}

Here,

  • Navigated each item in the expense array using map function, created table row (tr) for each entry using transform function and finally set the returned array in expenses variable.

  • Set the key attributes for each row with the index value of the item.

  • Used expenses array in the JSX expression to include the generated rows.

  • Used getTotalExpenses method to find the total expense amount and add it into the render method.

The complete source code of the ExpenseListUsingForLoop component is as follows −

import React from 'react'
class ExpenseListUsingForLoop extends React.Component {
   getTotalExpenses() {
      var items = this.props['expenses'];
      var total = 0;
      for(let i = 0; i < items.length; i++) {
         total += parseInt(items[i]);
      }
      return total;
   }
   render() {
      var items = this.props['expenses'];
      var expenses = []
      expenses = items.map(
         (item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item}</td></tr>)
      var total = this.getTotalExpenses();
      return <table>
         <thead>
            <tr>
               <th>Item</th>
               <th>Amount</th>
            </tr>
         </thead>
         <tbody>
            {expenses}
         </tbody>
         <tfoot>
            <tr>
               <th>Sum</th>
               <th>{total}</th>
            </tr>
         </tfoot>
      </table>
   }
}
export default ExpenseListUsingForLoop

Next, update the App component (App.js) with ExpenseListUsingForLoop component.

import ExpenseListUsingForLoop from './components/ExpenseListUsingForLoop';
import './App.css';
function App() {
   var expenses = [100, 200, 300]
   return (
      <div>
         <ExpenseListUsingForLoop expenses={expenses} />
      </div>
   );
}
export default App;

Next, add include a basic styles in App.css.

/* Center tables for demo */
table {
   margin: 0 auto;
}
div {
   padding: 5px;
}
/* Default Table Style */
table {
   color: #333;
   background: white;
   border: 1px solid grey;
   font-size: 12pt;
   border-collapse: collapse;
}
table thead th,
table tfoot th {
   color: #777;
   background: rgba(0,0,0,.1);
   text-align: left;
}
table caption {
   padding:.5em;
}
table th,
table td {
   padding: .5em;
   border: 1px solid lightgrey;
}

Next, check the application in the browser. It will show the expenses as shown below −

List and Keys

Finally, open the developer console and find that the warning about key is not shown.

Key and index

We learned that the key should be unique to optimize the rendering of the component. We used index value and the error is gone. Is it still correct way to provide value for the list? The answer is Yes and No. Setting the index key will work in most situation but it will behave in unexpected ways if we use uncontrolled component in our application.

Let us update our application and add two new features as specified below −

  • Add an input element to each row adjacent to expense amount.

  • Add a button to remove the first element in the list.

First of all, add a constructor and set the initial state of the application. As we are going to remove certain item during runtime of our application, we should use state instead of props.

constructor(props) {
   super(props)
   this.state = {
      expenses: this.props['expenses']
   }
}

Next, add a function to remove the first element of the list.

remove() {
   var itemToRemove = this.state['expenses'][0]
   this.setState((previousState) => ({
      expenses: previousState['expenses'].filter((item) => item != itemToRemove)
   }))
}

Next, bind the remove function in the constructor as shown below −

constructor(props) {
   super(props)
   this.state = {
      expenses: this.props['expenses']
   }
   this.remove = this.remove.bind(this)
}

Next, add a button below the table and set remove function in its onClick action.

render() {
   var items = this.state['expenses'];
   var expenses = []
   expenses = items.map(
      (item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item}</td></tr>)
   var total = this.getTotalExpenses();
   return (
      <div>
         <table>
            <thead>
               <tr>
                  <th>Item</th>
                  <th>Amount</th>
               </tr>
            </thead>
            <tbody>
               {expenses}
            </tbody>
            <tfoot>
               <tr>
                  <th>Sum</th>
                  <th>{total}</th>
               </tr>
            </tfoot>
         </table>
         <div>
            <button onClick={this.remove}>Remove first item</button>
         </div>
      </div>
   )
}

Next, add an input element in all rows adjacent to amount as shown below −

expenses = items.map((item, idx) => <tr key={idx}><td>item {idx + 1}</td><td>{item} <input /></td></tr>)

Next, open the application in your browser. The application will be render as shown below − Key and Index

Next, enter an amount (say, 100) in the first input box and then click "Remove first item" button. This will remove the first element but the entered amount will be populated in the input next to second element (amount: 200) as shown below −

Key and Index

To fix the issue, we should remove the usage of index as key. Instead, we can use unique id representing the item. Let us change the item from number array to object array as shown below (App.js),

import ExpenseListUsingForLoop from './components/ExpenseListUsingForLoop';
import './App.css';
function App() {
   var expenses = [
      {
         id: 1,
         amount: 100
      },
      {
         id: 2,
         amount: 200
      },
      {
         id: 3,
         amount: 300
      }
   ]
   return (
      <div>
      <ExpenseListUsingForLoop expenses={expenses} />
      </div>
   );
}
export default App;

Next, update the rendering logic by setting item.id as key as shown below −

expenses = items.map((item, idx) => <tr key={item.id}><td>{item.id}</td><td>{item.amount} <input /></td></tr>)

Next, update the getTotalExpenses logic as shown below −

getTotalExpenses() {
   var items = this.props['expenses'];
   var total = 0;
   for(let i = 0; i < items.length; i++) {
      total += parseInt(items[i].amount);
   }
   return total;
}

Here, we have changed the logic to get amount from object (item[i].amount). Finally, open the application in your browser and try to replicate the earlier issue. Add a number in first input box (say 100) and then click Remove first item. Now, the first element gets removed and also, the input value entered is not retained in next input box as shown below &minuss;

Key and Index
Advertisements