Code Review: Use A Good Key When Rendering Lists In Your React

When I can use Index as Key, and when I should generate another value for it
Huy Ngo
Jul 07, 2021

Rendering a list of items is a common scenario for most UI these days. In its official document, React states key as an essential property for your list. In this post, I’ll summarize some notes on its usage.

Index Is Good Enough For Some Cases

Static Lists

Suppose you only display items without any chance to reorder the list, such as deleting an object, filtering, or moving things around. In that case, index is a safe and easy way for key.

Lists of Stateless Components

If your items are stateless components, it’s also OK to use index as key even though you have to change their orders.

Although this method is not optimized for render performance, it doesn’t impact directly to user experience. If your list is small and the child components are not so complex, you can use index as key. (You can read more about how React uses key to optimize the rendering process in its official document).

In the sample below, I have a list of items with different labels and colors. I stored that info as a state of the list and used stateless components for rendering its children. When I delete some items, the labels and colors are still matching well. It means index works well as a key in this case.

import { useState } from 'react';
import { getColor } from './utils';

export const ListOfStatelessComponents = () => {
  const [values, setValues] = useState([
    {
      name: 'Item 1',
      colorIndex: 0,
    },
    {
      name: 'Item 2',
      colorIndex: 0,
    },
    {
      name: 'Item 3',
      colorIndex: 0,
    },
    {
      name: 'Item 4',
      colorIndex: 0,
    },
    {
      name: 'Item 5',
      colorIndex: 0,
    },
  ]);

  const del = (index) => {
    const newValues = [...values];
    newValues.splice(index, 1);
    setValues(newValues);
  };

  const countClick = (index) => {
    const newValues = [...values];
    newValues[index].colorIndex++;
    setValues(newValues);
  };

  return (
    <>
      {values.map((value, index) => (
        <p
          key={index}
          style={{ backgroundColor: `${getColor(value.colorIndex)}` }}
        >
          <span onClick={() => countClick(index)}>{value.name}</span>
          <button onClick={() => del(index)}>Delete</button>
        </p>
      ))}
    </>
  );
};

StatelessItems

When To Avoid Using Index

If your items are stateful components and users can change their orders, using index as key will mess up states of items.

The sample below has the same functionality as the above one. The difference is I moved the color code into state of child components. It means I’m now using stateful components instead of stateless. When I delete some items, the colors are all messed up.

import { useState } from 'react';
import { getColor } from './utils';

const ChildItem = ({ value, onDelete, ...rest }) => {
  const [colorIndex, setColorIndex] = useState(0);

  return (
    <p style={{ backgroundColor: `${getColor(colorIndex)}` }} {...rest}>
      <span onClick={() => setColorIndex(colorIndex + 1)}>{value}</span>
      <button onClick={onDelete}>Delete</button>
    </p>
  );
};

export const ListOfStatefulComponents = () => {
  const [values, setValues] = useState([
    'Item 1',
    'Item 2',
    'Item 3',
    'Item 4',
    'Item 5',
  ]);

  const del = (index) => {
    const newValues = [...values];
    newValues.splice(index, 1);
    setValues(newValues);
  };

  return (
    <>
      {values.map((value, index) => (
        <ChildItem key={index} value={value} onDelete={() => del(index)} />
      ))}
    </>
  );
};

StatefulItems

How To Deal With It

When you render a list of stateful components and want to re-order them, your data must have a unique value to use as index. If it hasn’t, you should generate one.

In the sample below, my list is an array of strings that don’t have any unique property. I must convert each item into an object with a newly generated ID, which became key for my stateful child components. This ID ensures correct states when I re-order the list.

import { useState } from 'react';
import { getColor } from './utils';

const ChildItem = ({ value, onDelete, ...rest }) => {
  const [colorIndex, setColorIndex] = useState(0);

  return (
    <p style={{ backgroundColor: `${getColor(colorIndex)}` }} {...rest}>
      <span onClick={() => setColorIndex(colorIndex + 1)}>{value}</span>
      <button onClick={onDelete}>Delete</button>
    </p>
  );
};

const generateId = (() => {
  let counter = 1;
  return () => `id_${++counter}`;
})();

export const ListOfStatefulComponentsWithCorrectKeys = () => {
  const [values, setValues] = useState(
    ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'].map((value) => {
      return { value, id: generateId() };
    })
  );

  const del = (index) => {
    const newValues = [...values];
    newValues.splice(index, 1);
    setValues(newValues);
  };

  return (
    <>
      {values.map((item, index) => (
        <ChildItem
          key={item.id}
          value={item.value}
          onDelete={() => del(index)}
        />
      ))}
    </>
  );
};

StatefulItemsCorrectKey

Don’t Generate ID Within Render

Don’t generate ID within render. This action is a bad practice for many reasons:

  • The keys will change every render. It forces React to see the items as newly added and always re-render.
  • Generate ID could be an expensive task for rendering action.

The ID should be a permanent value for your list. It should be generated at the caller (if you use props) or go along with your state.

// DON'T: generate key when render
const MyList = ({ items }) => {
  return (
    <>
      {items.map(item => (
        <p key={generateId()}>
          {item.value}
        </p>
      ))}
    </>
  );
}

// DO: generate ID at caller level if the list was passed by props
const Parent = () => {
  const renderingItems = values.map(value => {
    return {
      id: generateId(),
      value,
    }
  })
  return (
    ...
    <MyList items={renderingItems} />
    ...
  )
}

const MyList = ({ items }) => {
  return (
    <>
      {items.map(item => (
        <p key={item.key}>
          {item.value}
        </p>
      ))}
    </>
  );
}

// DO: If the list stored in a state, generate ID when the item was added in
const MyList = () => {
  const [renderingItems, setRenderingItems] = useState(values.map(value => {
    return {
      id: generateId(),
      value,
    }
  }));

  return (
    <>
      {renderingItems.map(item => (
        <p key={item.key}>
          {item.value}
        </p>
      ))}
    </>
  );
}

Conclusion

  • If your data has a unique property, use it.
  • If you don’t need to re-order your list, using index is good enough.
  • If you need to re-order your list and your child components are stateless, it’s okay to use index although not optimized for render performance.
  • In other cases, you have to generate a unique value for your list data first. This value should be permanent and shouldn’t change in renders.

=====

This article is part of my series about Code Review, which talks about my notes while reviewing code. Each of them could be tiny or nothing special for some people. But because a slight mistake can cost developers a lot to figure out, it could be helpful for others to mind all the possibilities.

I’ve put the samples above in a Github repository in case anyone wants to try them out.