correctness/each-key · キー付き each ブロック
動的なデータを回す {#each} にはキーを付けましょう。
重大度: warning · カテゴリ: correctness
チェック内容
キーのない {#each} ブロックを検出します。次の形は対象外とします。
- 定数のインライン配列リテラル(
{#each [1, 2, 3] as n})。長さが固定で並べ替えが起きないため、キーを付けても意味がありません。 - 同じリテラルをコンポーネント自身のスクリプトで名前付きにしたもの(
const tabs = [ … ]、export なし)。ただしファイル内で変更される余地がない場合に限ります。つまり、ほかの参照がすべてメンバーの読み取り(tabs.length。(tabs as readonly string[]).includes(x)のような型アサーション越しも含みます)、{#each tabs …}、for (const t of tabs)のループ、新しい配列へのコピー([...tabs, extra])のいずれかである場合です。変更するメソッド呼び出し(tabs.push)、それを介した代入、tabsの受け渡しや、配列リテラル以外でのスプレッドがあれば検出対象のままです。 - 同じリテラルをプロジェクト内のモジュールから import したもの(
export const TABS = [ … ]。as constやsatisfies付きも含む)。$libなどのkit.alias、相対パス、バレルの再エクスポートを経由しても解決し(拡張子を省いた runes モジュール、たとえばstate.svelte.tsを指す$lib/state.svelteも含む)、TABSとしてもimport * as nsのns.TABSとしても回せます。モジュール内で書き換えられておらず(import したものかグローバルの関数・コンストラクタに渡すz.enum(TABS)やnew Set(TABS)はそのモジュールでは読み取りとみなし、そのモジュール自身が宣言する関数に渡す場合は読み取りとみなしません。モジュール内のconstの別名(const labels = custom ?? TABS)も同じ読み取りに限ります)、コンポーネント側も上記の読み取りだけである場合に限ります。パッケージや JSON ファイルからの import、配列リテラルではない export(LIST.map(…)やスプレッドを含むもの)は検出対象のままです。同じリストを import している別のモジュールからの書き換えは確認しません。 - アイテムを取らない each(
{#each { length: 8 }, i}という「N 回描画する」パターン)。キーを付けられるようなアイテムの同一性が存在せず、唯一付けられる index 自体をキーにしても無意味です。 - 長さのみのリスト(
Array(n)、[...Array(n)]、Array.from({ length: n })、Array.from(new Array(n))、それらに.fill(x)や.keys()を続けたもの、as _のようにアイテムを受け取る{ length: n })。順序に意味のない固定長のプレースホルダー/スケルトンリストであり、キーを付けても意味がありません。
なぜ重要か
キーがないと、リストの並べ替えや要素の挿入、削除が起きたとき、Svelte は末尾でのノードの追加と削除で対応し、途中にある DOM ノードのデータを書き換えます。そのため要素の状態(フォーカス、入力、トランジション)がアイテムではなく位置に張り付いてしまい、余計な処理も発生します。キーがあれば、Svelte は正しいノードを挿入、移動、削除できます。
修正方法
{#each items as item (item.id)}
<li>{item.name}</li>
{/each}
モードによる違い
ありません。このルールが読むのは同じ .svelte / .ts のソースファイルなので、CLI、Vite プラグインのビルド、ライブダッシュボードの静的ベースラインのいずれでも結果は同一で、レンダリング済み HTML の解析で再評価されることもありません。--route で実行範囲を絞ると、このルールは動きません。コンポーネントスコープのルールには、検出を紐づけるルートが無いためです。
無効化
個別に抑制するには、対象行の直前に <!-- svelte-vitals-disable-next-line correctness/each-key --> を置きます。ルールごと無効化するには、次のように設定します。
export default {
rules: {
'correctness/each-key': 'off'
}
};