【React】 リストをレンダリングする際のkeyの必要性

Reactでfor文等を利用し、リストで要素を出力すると、keyの設定をするべきという警告が表示されます。ではkeyを使用することで何が良いのでしょうか。

keyの役割

リストの要素におけるkeyの役割は、Reactがそれぞれの要素を追尾できるようになるということです。これは、特に要素の追加や削除時に発生します。つまり、keyが必要になるのは、Reactで要素を配列で描画するときです。配列の要素が変化したときに、Reactがうまく追尾できるとパフォーマンスの向上ができます。

例えば、TODOリストのようなものを考えてみましょう。まずはkeyを使用しないパターンを実装してみます。

App.jsx
import TodoItem from './components/TodoItem';
import { useCallback } from 'react';

export default function App() {
  const [todos, setTodos] = useState([]);

  const addTodo = (e) => {
    e.preventDefault();

    setTodos([...todos, {
      id: new Date().getTime(),
      content: e.target.content.value,
    }]);

    e.target.content.value = "";
  };

  const deleteTodo = useCallback((index) => {
    setTodos(
        todos.filter((_, i) => i !== index)
    );
  }, [todos]);

  const renderTodos = () => {
    return todos.map((e, i) =>
      <TodoItem content={e.content} deleteTodo={() => {deleteTodo(i)}} />
    );
  }

  return (
    <div className="App">
      <ul>
        {renderTodos()}
      </ul>

      <form onSubmit={addTodo} method="post">
        <input type="text" name="content" placeholder="Todoの内容" />
        <input type="submit" value="追加" />
      </form>
    </div>
  );
}
TodoItem.jsx
/**
 * @typedef {{
 *  content: string
 *  deleteTodo: () => void
 * }} Props
 */

/**
 * @param {Props} props
 */
export default function TodoItem(props) {
    return (
        <li>
            <span style={{"marginRight": 12}}>{props.content}</span>
            <button onClick={props.deleteTodo}>削除</button>
        </li>
    );
}

TodoItemは表示とボタンクリック時に呼ぶべき関数を受け取って表示するだけです。

App.jsxが少し長いですが細かく読む必要はありません。追加、削除、表示の3つの関数があることと、renderTodosの中身を把握すれば十分です。

実際に動作させると、特に問題なく動作します。しかし、consoleの表示を見ると、削除した際に、それより後ろの項目が全て再レンダリングされるということがわかります。

これは、例えば

  • aaa
  • bbb
  • ccc

を表示していたとすると、ここからaaaの項目を削除した際に、

  • aaa → bbb
  • bbb → ccc
  • ccc → 消滅

という処理がされるためです。たしかに全ての値が変化しています。

keyを使って無駄な更新を防止

そこで、keyを使うことで項目を追尾し、無駄なDOMの更新を防ぎます。

const renderTodos = () => {
    // key={e.id} を追加
    return todos.map((e, i) =>
        <TodoItem content={e.content} deleteTodo={() => {deleteTodo(e.id)}} key={e.id} />
    );
}

こうすることで、

  • aaa → 消滅
  • bbb → 変化なし
  • ccc → 変化なし

という風に処理させることができ、DOMの再レンダリングを防ぐことができます。

keyに使う値

keyに使うべき値は、

  • その一連のリストの中でユニークであること。例えばUIDなど
  • 不変であること

を満たしていることが推奨されます。ユニークでない (他の項目とかぶる) 場合、うまく項目を追尾できない場合があります。

// GOOD 固有の変化しないIDを利用している
return todos.map(e =>
    <TodoItem key={e.id} />
);

// BAD ユニークではあるが値は毎回変化するので意味無し
return todos.map(e =>
    <TodoItem key={Math.random()} />
);

// BAD ユニークですら無い
return todos.map(e =>
    <TodoItem key={256} />
);

不変であることは、パフォーマンスを上げるにはほぼ必須です。しかし、UUIDなどのユニークな値が用意されていれば良いですが、ない場合は配列のインデックスを使わざるを得ません。配列のインデックスだと意味のある追尾ができないですが、最終手段として一応設定しておきましょう。

まとめ

keyは、Reactが要素を追尾できるようにするために使用するもので、リストのレンダリング時に用います。keyには、ユニークかつ変化しない値を用いるべきです。

無駄なレンダリングを防ぐために、必ず使用しておきましょう。

役に立ったらシェアしよう!