React.jsの地味だけど重要なkeyについて

  • 184
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

今回はReact.jsのVirtual DOM実装の中でもユーザーが意識するべき点のkeyについて書きたいと思います。

React.jsではPropにkeyという値を指定することが出来て、Componentのリストを表示するような時につけていないとdevelopment環境だとconsole.warn

Each child in an array should have a unique "key" prop. Check the render method of KeyTrap. See http://fb.me/react-warning-keys for more information.

と表示されます。

このkeyはVirtualDOMのdiffから実際のDOMに反映させるときに最小限の変更にするために使われます。

例えば

var KeySample = React.createClass({
  getInitialState() {
    return {
      list: [1,2,3,4,5]
    };
  },
  add() {
    this.setState({ list: [0].concat(this.state.list) });
  },
  render() {
    var list = this.state.list.map(function(i) { return <li key={i}>{i}</li> });
    return (
      <div>
        <ul>{list}</ul>
        <button onClick={this.add}>add</button>
      </div>
    );
  }
});

http://jsfiddle.net/koba04/tttLsmuL/

のようなComponentがあった時に、addするときに配列の要素にUniqueなIDが指定されていると先頭に要素が追加されたことがわかるので、そこだけ実際のDOMに反映することが出来ます。そうでないと1を0に2を1に...とそれぞれの要素の値を変更することになってしまい無駄になります。

そうすると↑のkeyの指定は問題があることがわかります。
addすると0が先頭に追加されているようになっているのですが、値をkeyにしているので一度0が追加された以降は何も追加されなくなります。
その裏側では、keyによってすでにある0と追加しようとしている0が比較されて差分がないので追加されないようになっています。

ちなみにwarnで下記のような警告がちゃんと出ます

Warning: flattenChildren(...): Encountered two children with the same key, .$0. Child keys must be unique; when two children share a key, only the first child will be used.

上のサンプルはkeyを外すと0が何個でも追加されるようになります。

ちなみにこの辺りと似た工夫はAngular.jsのtrack byやvue.jsのtrackbyなど別のフレームワークでも見ることが出来ます。

key must by unique

上記のconsole.warnにもある通り、keyにはそのリストの中で必ずユニークになる値を指定する必要があります。なのでkeyにする値の候補としてはユーザー一覧におけるユーザーIDなどになります。

配列のIndexをkeyに指定すると意味ないことがわかると思います。

ReactCSSTransitionGroup

React.jsにはCSSアニメーションのためのaddonがあるのですが、これは要素が1つの場合でもアニメーションさせる要素に必ずkeyをつける必要があったりします。
これはReactCSSTransitionGroupでは要素の追加・削除をtrackingしたいのでそのためにkeyを必要としているのだと思います。(実装は見てないですが...)

ReactCSSTransitionGroupについてはまだ後日改めて書く予定です。

おまけ

http://blog.arkency.com/2014/10/react-dot-js-and-dynamic-children-why-the-keys-are-important/

参考として最後にkeyにはまったというエントリーを紹介しておきたいと思います。

簡単に説明すると、

<CountriesComponent>
  <TabList />  {/* 国一覧 */}
  <TabList />  {/* ↑の都市一覧 */}
</CountriesComponent>

上記のようなComponentの構成になっていてTabListそれぞれがStateとしてactiveなtabのindexを持っています。

そして国一覧を変更した時に、都市一覧のactiveなindexを0にリセットしたいのにそのまま残ってしまってハマったという内容です。

activeはgetInitialStateで0にリセットされるようになっていて、国が変わった時に都市一覧のTabListは国に対応した都市のリストで再作成されてリセットされると思ったようなのですが、実際はTabList自体は再利用されて中のリストだけが更新されます。

つまりgetInitialStateが呼ばれずリセットされないというのがこの問題の原因です。

なのでTabListにkeyを指定して国が変わった時は都市のComponentが再生成されるようにすればOKということが解決方法の1つとして書かれています。

つまりkeyを明示することで新しくComponentを作るようにしています。

<CountriesComponent>
  <TabList key='countriesList'/>  {/* 国一覧 */}
  <TabList key={this.state.currentCountry} />  {/* ↑の都市一覧 */}
</CountriesComponent>

まぁ、この場合はブログでも言及されていますが、TabList内でactiveのStateを持つのではなくてContriesComponentが管理してPropでactiveな情報もTabListに渡してあげるのが正しいんじゃないかなと思っています。


というわけでReact.jsのkeyが持つ役割について説明しました。

明日はReact.jsのVirutal DOMについて書きたいと思います。
Virtual DOM Advent Calendarを見ればいいのですが、この流れで少し触れておきたいので簡単に...。

この投稿は 一人React.js Advent Calendar 201411日目の記事です。