reactでリストをレンダリングする際に気をつけること – key指定

reactにおいてリストを動的にレンダリングする場合、key要素に正しい値を指定すべきである。

未指定、配列のインデックスを使用する、ということを避け、なんらかのロジック(ここではshortidを使用)で採番した一意なIDを指定すべきである。

これをしない場合無駄なレンダリングが走るという問題もあるが、場合によっては以下に例示するように誤動作につながる。

正しく動作しない例1

以下のスクリプトは、ボタンをクリックするごとにリストの先頭に入力欄を1つづつ追加していく。

入力欄にはkeyとして配列のインデックスを指定している。

keyは一意である必要があり配列のインデックスは一意であるのだが、これは正しく動作しない。

import React, { useState, useCallback } from 'react';
import './App.css';

const shortid = require('shortid');

interface InputAreaProperty {
  id: string;
  createAt: string;
}

const InputArea = (props: { value: InputAreaProperty }) => (
  <div>
    <span><input /></span>
    <span>{props.value.createAt}</span>
  </div>
);

function App() {
  const [list, setList] = useState<InputAreaProperty[]>([]);
  const [title,] = useState("title");

  // ボタンをクリックするごとにリストに入力欄を追加
  const addList = useCallback(() => {
    const item: InputAreaProperty = {
      id: shortid.generate(),
      createAt: new Date().toLocaleTimeString(),
    };
    setList([item, ...list]);
  }, [list]);

  return (
    <div className="App">
      <button onClick={addList}>ADD LIST</button>
      <div>{title}</div>
      <div>
        {list.map((item, index) => (
          <InputArea key={index} value={item} />
        ))}
      </div>
    </div>
  );
}

export default App;

このスクリプトを動かし、入力欄に入力がある状態でボタンをクリックし入力欄を追加すると、以下のようになってしまう。

10:54:30の列に入力したはずの文字列が勝手に10:57:00の列に移動してしまう。

原因としては、10:54:30の列のkeyが変化してしまうところにある。

ボタンをクリックする前はもともとkey=0だったが、ボタンをクリックし入力欄を追加したことでkey=1となる。これにより入力欄が別物であると判断される。
代わりに10:57:00の列がkey=0となり、クリック前の10:54:30の列はクリック後の10:57:00の列に対応する。そのため10:54:30の列に入力した文字列が勝手に10:57:00に移動してしまう。

正しく動作しない例2

上のスクリプトからkey={inex}を削除してkeyを未指定にした場合も、同じ問題が生じる。

正しく動作する例

key={item.id}として、shortidを使用して採番した一意なIDを指定するとこの問題は解決する。

import React, { useState, useCallback } from 'react';
import './App.css';

const shortid = require('shortid');

interface InputAreaProperty {
  id: string;
  createAt: string;
}

const InputArea = (props: { value: InputAreaProperty }) => (
  <div>
    <span><input /></span>
    <span>{props.value.createAt}</span>
  </div>
);

function App() {
  const [list, setList] = useState<InputAreaProperty[]>([]);
  const [title,] = useState("title");

  // ボタンをクリックするごとにリストに入力欄を追加
  const addList = useCallback(() => {
    const item: InputAreaProperty = {
      id: shortid.generate(),
      createAt: new Date().toLocaleTimeString(),
    };
    setList([item, ...list]);
  }, [list]);

  return (
    <div className="App">
      <button onClick={addList}>ADD LIST</button>
      <div>{title}</div>
      <div>
        {list.map((item, index) => (
          <InputArea key={item.id} value={item} />
        ))}
      </div>
    </div>
  );
}

export default App;

このスクリプトを動かすと以下のようになる。11:11:30の列に入力した文字列は列を追加しても11:11:30の列に正しく入力されている。

先の例とは違い、11:11:30の列のkeyはボタンをクリックして入力欄を追加しても同値である。そのため、配列のインデックスをキーとした時のような混乱は生じない。

keyの指定を正しく行わなくても、問題がないパターンや、無駄なレンダリングは走るが機能上の問題はないパターンもある。ただ、原則はkeyに正しい値を設定することを心がけた方が良いだろう。

コメント

タイトルとURLをコピーしました