Part 3 · 画面に動きをつける
22 / 64 章
key の細かい話
index を使っていい場合、key は props ではない、など
前の章で、key には id を使うという結論まで来ました。 ふだん書くぶんには、それで足ります。
この章はそのあとに必ず出てくる疑問を片づけます。 「index を使ってはいけないの?」「key って props じゃないの?」
読み流して、必要になったときに思い出せれば十分です。
これは入力欄だけの話ではない
分かりやすさのために入力欄を使いましたが、同じことはstate を持つコンポーネントでも起きます。
- チェックを入れたはずの行と、別の行にチェックが付く
- 開いていたアコーディオン(開閉するリスト)が、別の項目で開いたままになる
- アニメーションが途中から始まる、あるいは動かない
React が「同じ行だ」と判断した箱の中身は、そのまま次のデータに引き継がれます。 key を間違えると、これが全部ズレます。
member-row.tsx
"use client";
import { Input } from "@/components/ui/input";
/**
* リストの 1 行。
*
* メモ欄に打った値は React の state ではなく、ブラウザの入力欄そのものが持っている。
* この「React が管理していない値」が、key を間違えたときに置き去りになる。
*/
export function MemberRow({ name }: { name: string }) {
return (
<div className="flex items-center gap-3 rounded-md border px-3 py-2">
<span className="w-16 shrink-0 font-medium">{name}</span>
<Input placeholder="メモを書いてみる" className="h-8" aria-label="メモを書いてみる" />
</div>
);
}index を key にしていい場合
いつでもダメというわけではありません。 次の条件を全部満たすなら、index で問題ありません。
- 並び替えが起きない
- 途中への追加や削除が起きない(末尾に足すだけなら大丈夫)
- 各行が入力欄や state を持っていない
たとえば、固定のメニュー項目を並べるだけのリストなら index で十分です。そもそも順番が変わらないので、index は事実上そのデータ固有の値になっています。
broken-list.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { MemberRow } from "./member-row";
type Member = { id: number; name: string };
const initialMembers: Member[] = [
{ id: 1, name: "さとう" },
{ id: 2, name: "すずき" },
{ id: 3, name: "たかはし" },
];
const newcomers = ["やまだ", "いとう", "わたなべ", "こばやし"];
export function BrokenList() {
const [members, setMembers] = useState(initialMembers);
const addToTop = () => {
const name = newcomers[members.length - 3];
if (!name) return;
// 先頭に追加する。既存のメンバーは 1 つずつ後ろにずれる
setMembers([{ id: members.length + 1, name }, ...members]);
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2.5">
{members.map((member, index) => (
// key に index を使っている。ここが問題
<MemberRow key={index} name={member.name} />
))}
</div>
<div className="flex gap-2">
<Button size="sm" onClick={addToTop}>
先頭にメンバーを追加
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setMembers(initialMembers)}
>
最初から
</Button>
</div>
</div>
);
}よくある勘違い
key は props ではない
key は React が内部で使うための特別な属性で、子コンポーネントには渡りません。 子で受け取ろうとしても、そこには何も入っていません。 行の中で id が必要なら、別の名前でもう一度渡します。
// id を子でも使いたいときは、key とは別に渡す
<MemberRow key={member.id} id={member.id} name={member.name} />一意でなければいけないのは「兄弟の中だけ」
ページ全体で一意である必要はありません。 同じ map から出てきた要素どうしで重複していなければ大丈夫です。 画面上の別々のリストで key が 1 どうしぶつかっていても、 何の問題もありません。
key に Math.random() を使ってはいけない
一意な値がほしくて Math.random() や Date.now() を使いたくなることがありますが、これは最悪の選択です。
// 絶対にやってはいけない
{members.map((member) => (
<MemberRow key={Math.random()} name={member.name} />
))}描き直されるたびに違う値になるので、React は毎回「全部が知らない行だ」と判断します。 つまり毎回すべての行が作り直されます。 遅くなるうえに、入力中の文字も state も毎回消えます。
メモ欄に打ってから、ボタンを押す
まず各行のメモ欄に何か打ってから、ボタンを押してください。
打った文字が消えました。リストの中身は 1 つも変えていません。 ただ描き直しただけです。 それでも key が毎回違うので、React は全部を別物とみなし、行ごと作り直しています。
同じように、メモ欄に打ってからボタンを押す
こちらも、メモ欄に打ってからボタンを押してください。
こちらは打った文字が残ります。 key が変わっていないので、React は同じ行だと判断し、 作り直さずにそのまま使います。
key は同じデータなら毎回同じ値でなければ、 名札としての意味がありません。
fixed-list.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { MemberRow } from "./member-row";
type Member = { id: number; name: string };
const initialMembers: Member[] = [
{ id: 1, name: "さとう" },
{ id: 2, name: "すずき" },
{ id: 3, name: "たかはし" },
];
const newcomers = ["やまだ", "いとう", "わたなべ", "こばやし"];
export function FixedList() {
const [members, setMembers] = useState(initialMembers);
const addToTop = () => {
const name = newcomers[members.length - 3];
if (!name) return;
setMembers([{ id: members.length + 1, name }, ...members]);
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2.5">
{members.map((member) => (
// データ自身が持つ id を key にする
<MemberRow key={member.id} name={member.name} />
))}
</div>
<div className="flex gap-2">
<Button size="sm" onClick={addToTop}>
先頭にメンバーを追加
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setMembers(initialMembers)}
>
最初から
</Button>
</div>
</div>
);
}理解できたか確かめる
確認クイズ
index を key にしても問題が起きないのは、次のうちどれ?
確認クイズ
key に Math.random() を使うと何が起きる?
確認クイズ
子コンポーネントの中で、その行の id を使いたい。どうする?
fixed-list.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { MemberRow } from "./member-row";
type Member = { id: number; name: string };
const initialMembers: Member[] = [
{ id: 1, name: "さとう" },
{ id: 2, name: "すずき" },
{ id: 3, name: "たかはし" },
];
const newcomers = ["やまだ", "いとう", "わたなべ", "こばやし"];
export function FixedList() {
const [members, setMembers] = useState(initialMembers);
const addToTop = () => {
const name = newcomers[members.length - 3];
if (!name) return;
setMembers([{ id: members.length + 1, name }, ...members]);
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2.5">
{members.map((member) => (
// データ自身が持つ id を key にする
<MemberRow key={member.id} name={member.name} />
))}
</div>
<div className="flex gap-2">
<Button size="sm" onClick={addToTop}>
先頭にメンバーを追加
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setMembers(initialMembers)}
>
最初から
</Button>
</div>
</div>
);
}この章のまとめ
- key の影響は入力欄だけではない。state を持つ部品すべてに及ぶ
- index を key にしてよいのは並び替えも追加も削除も起きないときだけ
- key は props ではない。子には届かない。 必要なら id を別に渡す
- 重複しなければよいのは兄弟の中だけ
Math.random()は毎回別物になるので使わない
fixed-list.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { MemberRow } from "./member-row";
type Member = { id: number; name: string };
const initialMembers: Member[] = [
{ id: 1, name: "さとう" },
{ id: 2, name: "すずき" },
{ id: 3, name: "たかはし" },
];
const newcomers = ["やまだ", "いとう", "わたなべ", "こばやし"];
export function FixedList() {
const [members, setMembers] = useState(initialMembers);
const addToTop = () => {
const name = newcomers[members.length - 3];
if (!name) return;
setMembers([{ id: members.length + 1, name }, ...members]);
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2.5">
{members.map((member) => (
// データ自身が持つ id を key にする
<MemberRow key={member.id} name={member.name} />
))}
</div>
<div className="flex gap-2">
<Button size="sm" onClick={addToTop}>
先頭にメンバーを追加
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setMembers(initialMembers)}
>
最初から
</Button>
</div>
</div>
);
}ここで Part 3 は終わりです
Part 3 では 3 つのことをやりました。押したら動く(イベント)、条件で出し分ける、並べる(リストと key)。
この 3 つで、画面の見た目はだいたい作れます。 ですが、まだ決定的に足りないものがあります。
fixed-list.tsx
"use client";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { MemberRow } from "./member-row";
type Member = { id: number; name: string };
const initialMembers: Member[] = [
{ id: 1, name: "さとう" },
{ id: 2, name: "すずき" },
{ id: 3, name: "たかはし" },
];
const newcomers = ["やまだ", "いとう", "わたなべ", "こばやし"];
export function FixedList() {
const [members, setMembers] = useState(initialMembers);
const addToTop = () => {
const name = newcomers[members.length - 3];
if (!name) return;
setMembers([{ id: members.length + 1, name }, ...members]);
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2.5">
{members.map((member) => (
// データ自身が持つ id を key にする
<MemberRow key={member.id} name={member.name} />
))}
</div>
<div className="flex gap-2">
<Button size="sm" onClick={addToTop}>
先頭にメンバーを追加
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setMembers(initialMembers)}
>
最初から
</Button>
</div>
</div>
);
}