JavaScript
React

Reactで繰り返し項目の独立性を高めるとkeyの設定に悩む

概要

配列をもとにしてこんなビューを作りたいときに試行錯誤してコンポーネントの独立性を高められたけれどkeyの設定に悩んでいる、という話です。

<ul>
  <li><span>#1 foo</span></li>
  <li><span>#2 bar</span></li>
  <li><span>#3 baz</span></li>
</ul>

どういう経緯でkeyの悩みに至ったか段階的に書いていきます。

以降に出てくるコード例の前提

出てくるコンポーネントはRootParentChildの3つです。
構造はこんな感じ。

<Root>
  <Parent>
    <Child><!-- ここが配列をもとに繰り返される -->
  </Parent>
</Root>

使うデータはこんな感じ。

class Content {
    constructor(id, text) {
        this.id = id;
        this.text = text;
    }
}

const contents = [
    new Content(1, 'foo'),
    new Content(2, 'bar'),
    new Content(3, 'baz')
];

Step.1 まずは何も考えずに書いてみた

const Root = ({ contents }) => <Parent contents={contents} />;

const Parent = ({ contents }) => <ul>
      {contents.map(x => <Child key={x.id} content={x}/>)}
    </ul>;

const Child = ({ content }) => <li>
      <span>#{content.id} {content.text}</span>
    </li>;

ReactDOM.render(<Root contents={contents} />, document.getElementById('root'));

Parentの定義にChildが含まれています。
つまりParentChildに依存しているということです。
この依存を解消したくなりました。

Step.2 Rootで構造を定義してみた

const Root = ({ contents }) => <Parent contents={contents}>
      {contents.map(x => <Child key={x.id} content={x}/>)}
    </Parent>;

const Parent = ({ children }) => <ul>
      {children}
    </ul>;

const Child = ({ content }) => <li>
      <span>#{content.id} {content.text}</span>
    </li>;

ReactDOM.render(<Root contents={contents} />, document.getElementById('root'));

ParentChildの構造をRootで定義するようにしました。
これでParentからChildへの依存を解消できました。

この時点で、ReactではParentChildのように独立したコンポーネントとRootのように構造化を司るコンポーネントに分けて設計した方がよいコードになりそうだなと思うようになりました。

ただ、これだとChildli要素を持っていて親(ul要素を持つコンポーネント)の存在を暗に示しています。
また、Parentli要素を持っていないので子の存在を暗に示しています。
ChildからParentli要素を移動させると独立性をもっと高められるような気がしました。

Step.3 ParentChildの独立性を高めた

const Root = ({ contents }) => <Parent contents={contents}>
      {contents.map(x => <Child key={x.id} content={x}/>)}
    </Parent>;

const Parent = ({ children }) => <ul>
      {React.Children.map(children, x =>
          <li key={x.props.content.id}>{x}</li>
        )}
    </ul>;

const Child = ({ content }) => <span>#{content.id} {content.text}</span>;

ReactDOM.render(<Root contents={contents} />, document.getElementById('root'));

ChildからParentli要素を移動しました。
これで暗黙的な親子の気配を取り除けたのでそれぞれ独立性が高まったと思います。

しかし、Parentli要素にkeyに設定している値が子コンポーネントのprops.content.idとなっています。
これは子コンポーネントに渡されるpropsの詳細を知っていることになり、完全には依存を解消できていません。

Step.4 keyになり得る値を引き回すルールを決めた

const Root = ({ contents }) => <Parent contents={contents}>
      {contents.map(x => <Child key={x.id} relayedKey={x.id} content={x}/>)}
    </Parent>;

const Parent = ({ children }) => <ul>
      {React.Children.map(children, x =>
          <li key={x.props.relayedKey}>{x}</li>
        )}
    </ul>;

const Child = ({ content }) => <span>#{content.id} {content.text}</span>;

ReactDOM.render(<Root contents={contents} />, document.getElementById('root'));

配列から作られる繰り返しのコンポーネントにはrelayedKeyという名前でkeyと同じ値を設定するようにルールを決めました。
childrenmapするとき、mapの前後でコンポーネントは1対1になるのでrelayedKeykeyとして使えます。

これでParentも独立性が高まりました。

もっとよい方法はないか?

Step.4で示したrelayedKeyルールの導入が今私が考えられる最善の方法です。
とはいえ単に名前を取り決めるというルールで縛っているだけなので納得はしていません。

もっとよい方法はありませんかね?|・`ω・)チラッ