mapメソッドに不可欠なunique "key" prop の役割

2 min read読了の目安(約2400字

配列に格納した要素をmapメソッドで一覧表示した時の事です。

コードは正常に動いている一方で警告メッセージが出現。

Each child in a list should have a unique "key" prop.
(配列の子要素それぞれにユニークキーを持たせないと!)

別になくても良くない?と思ったので、なぜ unique "key" prop が大切なのか調べてみました。

リストコンポーネントと Key

公式ドキュメントにはkeyの必要性について以下のような説明がありました。

Key は、どの要素が変更、追加もしくは削除されたのかを React が識別するのに役立ちます。配列内の項目に安定した識別性を与えるため、それぞれの項目に key を与えるべきです。

もちろん一意キーがあった方が識別性は高まると思います。ただ、リストの子要素それぞれにそれを設定しなくても一覧表示する分には問題がないとも考えてしまいます。

配列内で一意であればよくてグローバルに一意である必要はないみたいです。

Unique Key が必要な理由

一意キーがサポートされている理由はReactの仕組み(差分アルゴリズム)にありました。

デフォルトでは、DOM ノードの子に対して再帰的に処理を行う場合、React は単純に、両方の子要素リストのそれぞれ最初から同時に処理を行っていって、差分を見つけたところで毎回更新を発生させます。

どうやらReactでは、それぞれの木構造を同時に読み込み一つずつ上から下に処理するようです。
この仕組みから以下のような場合は問題がないようです。

<ul>
  <li>one</li>
  <li>two</li>
</ul>

<ul>
  <li>one</li>
  <li>two</li>
  <li>three</li>
</ul>

one,twoまでは一緒なのでスルー、最後のthreeに辿り着いた時に更新されます。

問題点は非効率

問題は以下のように子要素が先頭に追加されているような場合です。

<ul>
  <li>one</li>
  <li>two</li>
</ul>

<ul>
  <li>zero</li>
  <li>one</li>
  <li>two</li>
</ul>

Reactはこの差分を検知するシンプルに上から下に比べていきます。
この場合はonezero,twooneに更新され、最後にtwoが挿入されることになります。

これは確かに効率が悪い!

ReactはKey属性をサポートすることでこの問題を解決しているようです。

<ul>
  <li key="1">one</li>
  <li key="2">two</li>
</ul>

<ul>
  <li key="0">zero</li>
  <li key="1">one</li>
  <li key="2">two</li>
</ul>

一意キーの値によって新規の子要素かどうかの判別が着くので、新規でない子要素は保守されたまま更新することができます。

アプリの動作に支障は出るのか?

どんな問題が起こりうるのかを再現するためにあえて Key なしでアプリを作ってみました。

試しに午前9時の"おはよう"を"こんばんわ"に変更して削除してみます。
すると削除したはずの午前9時の"こんばんわ"が午後1時に引き継がれてしまいます。

一方で、最後の午後9時の"おはよう"を"こんばんわ"に変更して削除しても問題は発生しません。

先ほどの比較のように、先頭の子要素の変化があった場合は他の子要素との識別ができないためにこのようなエラーが発生してしまうと思われます。

Summary

最初にこの警告メッセージをみた時はあまりピンときませんでしたが今はものすごく納得しました。
配列内の子要素には一意キーを持たせること!これだけ覚えて寝ます。

Reference

Understanding the importance of the key prop in React

https://ja.reactjs.org/docs/lists-and-keys.html

https://ja.reactjs.org/docs/reconciliation.html#recursing-on-children