とある開発会社での千鳥の会話
ノブ「大吾、手隙か?」
大吾「手隙じゃ」
ノブ「ちょいと大吾に頼みたいことがある」
大吾「なんでもまかせろ」
ノブ「ユーザーが自由に入力フォームを追加できる機能を作ってくれんか」
ノブ「完成形はこんな感じじゃ。増やすボタンを押すと入力フォームが追加されるイメージじゃ」
大吾「こんなん簡単じゃな。任せときい」
カタカタカタカタカタカタカタカタカタカタカタカタ
大吾「ノブ〜できたぞ」
ノブ「おお!?はや早い。どれどれ見てみよか」
ノブ「ちゃんとボタンを押すと入力フォームが増えとるな」
ノブ「入力フォームに値を入れて確認してみようか」
ノブ「おいおいおい!!」
ノブ「idが1の入力フォームに入れた入力値が追加ボタンを押して出てくるidが2の入力フォームに移動しとるやないカイ!」
大吾「そんなんワシは知らん」
ノブ「ちょっと大吾の書いたコードを見せてくれんか」
大吾「ほいっ」
const Example: NextPage = () => {
const [inputId, setInputId] = useState<number[]>([1]);
const onClick = () => {
const copy = [...inputId];
copy.unshift(inputId.length + 1);
setInputId(copy);
};
return (
<ExampleContainer>
{inputId.map((i, index) => (
<div key={index}>
<p>id:{i}</p>
<input type="text" />
<br />
</div>
))}
<button onClick={onClick} className="button">
入力フォームを増やす
</button>
</ExampleContainer>
);
};
export default Example;
ノブ「どれどれどれ」
ノブ「この部分で追加ボタンを押したときに配列の先頭に値が増えるようにしてるんやな」
const [inputId, setInputId] = useState<number[]>([1]);
const onClick = () => {
const copy = [...inputId];
copy.unshift(inputId.length + 1);
setInputId(copy);
};
大吾「この部分の実装は、前にノブに教えたスプレット構文を使って配列のstateは更新してるぞ!」
ノブ「そうじゃなっ」
ノブ「この辺は問題なさそうやな」
ノブ「JSXに注目して見てよか」
<ExampleContainer>
{inputId.map((i, index) => (
<div key={index}>
<p>id:{i}</p>
<input type="text" />
<br />
</div>
))}
<button onClick={onClick} className="button">
入力フォームを増やす
</button>
</ExampleContainer>
ノブ「お!!!???」
ノブ「まてまてまてまて」
ノブ「JSX内のmapメソッドでindexをkeyに指定しないでくれぇ」
JSX内のmapメソッドでindexをkeyに指定しないでくれぇ
ノブ「JSX内のこの部分に注目せぇ」
ノブ「ここでinput
をラップしているdiv
タグのkey
にindex
を渡してるやろ」
ノブ「これが影響してさっきのような挙動になってしまってるんじゃ」
{inputId.map((i, index) => (
<div key={index}>
<p>id:{i}</p>
<input type="text" />
<br />
</div>
))}
ノブ「本来ならば新しく追加した入力フォーム(idが2)は空でないといけないはずなんだがindex
をkey
に指定した影響でidが1のフォームに入力した値が入ってきてしまっとる」
ノブ「ちなみにReactの公式ドキュメントでもこの部分については言及してるんじゃ」
要素の並び順が変更される可能性がある場合、インデックスを key として使用することはお勧めしません。パフォーマンスに悪い影響を与え、コンポーネントの状態に問題を起こす可能性があります。
なぜこのような挙動になるのか
大吾「ふむふむ。ノブが言いたいことはよう分かった。でもどうしてこんな挙動になってしまうんじゃ」
ノブ「配列のindex
って0から始まるじゃろ?」
ノブ「そのためindex
が0番目の要素にidは1番ですという入力した値が紐付いてしまってるんじゃ」
ノブ「結果として新しく増えたidが2の入力フォーム(配列の0番目)に値がid:1で入力した値が入ってきてしまってるってことじゃな」
大吾「入力フォームのkeyが配列のindex番号に紐付いたことによってこの挙動になってしまってるわけか」
ノブ「画面で見てみるとこんな感じやな」
解決策
大吾「じゃあこれを解決するにはどうすれば良いんじゃ」
ノブ「index
ではなくid
などのユーニクな値をkey
に指定するれば解決する」
ノブ「今回で言えばこんな感じじゃ」
const Example: NextPage = () => {
const [inputId, setInputId] = useState<number[]>([1]);
const onClick = () => {
const copy = [...inputId];
copy.unshift(inputId.length + 1);
setInputId(copy);
};
return (
<ExampleContainer>
{inputId.map((i) => (
<div key={i}> // keyにid(配列の値)を入れる
<p>id:{i}</p>
<input type="text" />
<br />
</div>
))}
<button onClick={onClick} className="button">
入力フォームを増やす
</button>
</ExampleContainer>
);
};
export default Example;
大吾「おおっ!!正しく動いとる」
ノブ「APIとかのレスポンス値だと多くの場合はオブジェクト形式でid
が付いて返ってくるからそれをmap
を利用する場合はkey
で指定するイメージじゃな」
const response = [
{
id: 1, // keyとしてidを指定する
value: 'りんご',
},
{
id: 2,
value: 'ばなな',
},
{
id: 3,
value: 'ぶどう',
},
];
大吾「なるほど。じゃあmap
メソッド内でindex
はほとんど使っちゃダメんか?」
mapメソッドのkeyにindexを指定してもよい場合
ノブ「map
メソッドのkey
にindex
を指定してもよい場合を説明するぞ」
ノブ「React公式ドキュメントからリンクが貼られているIndex as a key is an anti-patternではな」
To help you decide, I put together three conditions which these examples > have in common:
- the list and items are static–they are not computed and do not change;
- the items in the list have no ids;
- the list is never reordered or filtered.
When all of them are met, you may safely use the index as a key.
日本語訳をすると
- 配列と要素が静的で計算が行われない場合
- 配列内の要素がIDを持ってない場合
- 配列要素は順番が変わったりフィルターされることがない
コレらを全て満たしている場合に安全にindex
にkey
を使用することができる。
大吾「なるほどやな」
大吾「今後はmap
メソッドを使う際はindex
はできる限り使わんようにするわ」
最後に
ノブ「他にも色々な記事を書いているのでぜひ読んでくれぇぇぇぇぇ」