【ITニュース解説】Loops in Svelte — {#each}, Keys, and Building a Todo App

2025年09月07日に「Dev.to」が公開したITニュース「Loops in Svelte — {#each}, Keys, and Building a Todo App」について初心者にもわかりやすいように丁寧に解説しています。

作成日: 更新日:

ITニュース概要

Svelteの`{#each}`は、配列などのコレクションから動的なリストUIを効率的に作成する機能だ。要素の繰り返し、データの分解、インデックス使用、空リスト対応、ネスト、`$state`でのリアクティブ更新、キーによる安定した描画について解説する。

ITニュース解説

現代のアプリケーションにおいて、一つの要素だけを表示する場面はほとんどない。例えば、Todoアプリには複数のタスクが、メールボックスには多くのメールが、SNSのフィードには数え切れない投稿が並ぶ。これらすべてを一つずつ手作業で記述することは現実的ではない。コンピューターにこの繰り返し作業を任せるのが、Svelteにおけるループ機能の役割だ。Svelteのループは、一つのテンプレートを設計すれば、必要なだけ複製して表示してくれる。

この解説では、Svelteでリストやコレクションデータを効率的に表示するための主要なループ機能について詳しく説明する。

{#each}ブロック:リストの基本

Svelteで最も基本的なループが{#each}ブロックである。これは配列の各要素を順番に処理し、対応するHTML要素を生成する。 例えば、fruitsという名前の果物の配列がある場合を考える。Svelteのスクリプト部でlet fruits = $state(['Apple', 'Banana', 'Cherry']);のように配列を宣言する。ここで$stateを使用すると、このfruits配列がリアクティブになる。つまり、配列の内容が変更されると、自動的にUIも更新される。 HTMLテンプレート部では、<ul>タグの中に{#each fruits as fruit}と記述し、その内部に<li>{fruit}</li>と書く。これにより、fruits配列の各要素がfruitという変数に順次代入され、それぞれの果物名がリストアイテムとして表示される。最終的に「Apple」「Banana」「Cherry」と書かれた順序なしリストが生成される。この仕組みは、配列の各要素に対して同じ構造のHTMLを繰り返し生成する際に非常に便利だ。

ループ内の分割代入

配列の要素は、単なる文字列だけでなく、複数のプロパティを持つオブジェクトである場合も多い。{#each}ブロックでは、このようなオブジェクトから必要なプロパティだけを直接取り出す「分割代入」が利用できる。 例えば、todosという名前のTodoアイテムの配列を考える。各Todoアイテムはidtextdoneといったプロパティを持つオブジェクトである。 {#each todos as { text, done }}のように記述することで、ループ内でtextdoneプロパティを直接変数として使える。これにより、todo.texttodo.doneと書く代わりに、textdoneと簡潔に記述できるようになる。これはコードの可読性を高め、冗長な記述を避けるのに役立つ。例えば、<li>{text} {done ? '✅' : '❌'}</li>と書けば、完了したTodoには✅、未完了には❌が表示される、整理されたTodoリストが作れる。

インデックスの利用

リストの項目が何番目にあるか(0番目、1番目、2番目など)を知りたい場合もある。{#each}ブロックでは、ループ変数に加えてインデックスも取得できる。 {#each items as item, i}のように記述することで、itemには現在の要素が、iにはその要素のインデックス(0から始まる)が代入される。このiを使えば、<li>{i + 1}. {item}</li>のようにリストの項目に連番を振ることが可能になる。これにより、視覚的に分かりやすい番号付きリストが簡単に実現できる。

空のリストの処理

配列が空の場合、デフォルトでは何も表示されない。しかし、多くの場合、「項目がありません」のようなメッセージを表示したいことがある。Svelteの{#each}ブロックには、{:else}構文が用意されており、これを活用できる。 {#each books as book} ... {:else} ... {/each}という形式で記述すると、books配列に要素がある場合はループ内の処理が実行され、要素が一つもない場合は{:else}ブロック内のコンテンツが表示される。これにより、リストが空の場合でもユーザーに適切な情報を提供できる。

ネストされたループ

ループは入れ子にすることも可能だ。つまり、ループの中にさらに別のループを配置できる。これは、カテゴリとその中のアイテムのように、階層的なデータを表示する際に非常に有効だ。 例えば、categoriesという配列があり、各カテゴリはnameとそのカテゴリに属するitemsの配列を持つオブジェクトである場合を考える。 まず、外側の{#each categories as category}でカテゴリをループし、その内部で<h2>{category.name}</h2>としてカテゴリ名を表示する。次に、その<h2>の下に、内側の{#each category.items as item}を使って、各カテゴリのitems配列をループし、<li>{item}</li>として項目を表示する。このようにすることで、「Fruits」「Apple」「Banana」「Vegetables」「Carrot」「Lettuce」のように、カテゴリごとに整理されたリストを構築できる。ただし、コードが複雑になりすぎる場合は、小さなコンポーネントに分割することを検討すると良い。

リアクティビティの再確認

Svelteの$state機能を使うと、配列やオブジェクトがリアクティブになる。これは、配列のpushpopspliceなどのメソッドによる変更(ミューテーション)や、オブジェクトのプロパティの更新が、自動的にUIに反映されることを意味する。配列全体を新しいものに置き換える(再代入)必要はないが、もちろん再代入も機能する。 例えば、Todoリストに新しいTodoを追加する際に、todos.push(v);のように配列に直接要素を追加するだけで、UIは即座に新しいTodoを表示する。todos = [...todos, v];のようにスプレッド構文で新しい配列を作成し、再代入する方法も可能であり、コードの意図を明確にするために好まれる場合もある。オブジェクトの場合も同様に、person.likes++のようにプロパティを直接変更するだけでUIが更新される。これにより、開発者はUIの更新について深く考えることなく、データの変更に集中できる。

リストのスタイリング

ループ内の各アイテムは、その状態に基づいて見た目を動的に変えることができる。条件分岐とCSSスタイルを組み合わせることで、リストアイテムに視覚的な意味を持たせることが可能だ。 例えば、Todoアイテムが完了しているか(done: true)どうかによって、テキストの色やアイコンを変えることができる。{#each todos as { text, done }}のように分割代入でtextdoneを取得し、style="color: {done ? 'green' : 'red'}"{done ? '✅' : '❌'}のように条件付きでスタイルやテキストを適用する。これにより、完了したタスクは緑色の✅、未完了のタスクは赤色の❌で表示され、一目で状態を把握できるリストが作成できる。

キーの重要性

リストの表示において「キー」は非常に重要な役割を果たす。リストのアイテムが追加、削除、または順序変更された際に、SvelteがどのHTML要素がどのデータアイテムに対応するかを正確に識別するためにキーが必要となる。 {#each people as person (person.id)}のように、eachブロックの後に括弧で囲んでperson.idのような一意の識別子を指定する。このidが「キー」となる。 キーがない場合、Svelteはリストアイテムの順序が変更された際に、既存のHTML要素を再利用してしまうことがある。この再利用が間違ったデータに対して行われると、例えば入力フィールドのフォーカスがおかしな動きをしたり、アニメーションが意図しない挙動を示したりするなどの問題が発生する可能性がある。キーを指定することで、Svelteは各アイテムをそのキーで識別し、データの変化に応じて正しいHTML要素を移動、削除、または更新するようになるため、UIの動作が安定する。データのIDなど、そのデータアイテムを一意に識別できる値を使うのが一般的だ。

パフォーマンスに関する注意

「数千ものアイテムをループで表示すると遅くなるのではないか」と心配する人もいるかもしれない。しかし、Svelteはループを非常に効率的なコードにコンパイルする。実際には、データが変更された部分だけを特定してUIを更新するため、ほとんどのアプリケーションではパフォーマンスを気にする必要はない。非常に大規模なリスト(数万行以上)を扱う場合は、画面に表示されている部分だけをレンダリングする「仮想化」のような高度な最適化を検討することもあるが、これは特別なケースだ。基本的には、明確なループを記述し、必要に応じてキーを追加すれば、Svelteが残りの処理を効率的に行ってくれる。

ミニプロジェクト:ショッピングカート

これまでに学んだすべてのループ機能を組み合わせて、シンプルなショッピングカートを構築する例を見てみよう。 ショッピングカートのアイテムはcartという配列で管理され、各アイテムはidnameqtyを持つオブジェクトである。 新しいアイテムの追加は、入力フィールドに名前を入力し、「Add」ボタンをクリックすることで行われる。この際、Date.now()のようなユニークなIDを生成し、cart = [...cart, { id, name: newItem, qty: 1 }];のように新しい配列を再代入してアイテムを追加する。 各アイテムはリストとして表示され、その横には「+」ボタンと「Remove」ボタンが配置される。「+」ボタンはアイテムのqtyを増やす役割を持ち、cart = cart.map(it => it.id === item.id ? { ...it, qty: it.qty + 1 } : it);のようにmapメソッドを使って新しい配列を作成し、cartを再代入することで数量を更新する。「Remove」ボタンはcart = cart.filter(it => it.id !== item.id);のようにfilterメソッドで特定のアイテムを除外した新しい配列を生成し、cartを再代入することでアイテムを削除する。 また、{#each cart as item, i (item.id)} ... {:else} <li>Cart is empty.</li> {/each}のようにインデックス、キー、空リストの処理を全て適用することで、追加、削除、数量変更、空表示がすべて機能する動的なショッピングカートが完成する。

まとめ

Svelteのループ機能は、動的なUIを構築するための強力なツールである。 {#each}ブロックは配列の各要素を繰り返して表示し、分割代入を使えばオブジェクトのプロパティを簡潔に取り出せる。iを使うことでインデックスも取得でき、{:else}ブロックで空のリストを適切に処理できる。ネストされたループは階層的なデータを扱う際に便利だが、複雑になりすぎないよう注意が必要だ。 $stateを使ったリアクティビティは、配列やオブジェクトの変更(ミューテーションや再代入)が自動的にUIに反映されるため、データの扱いは非常に直感的である。そして、リストアイテムの安定した挙動のためには「キー」の指定が不可欠である。Svelteのループは非常に効率的に動作するため、ほとんどのケースでパフォーマンスを心配する必要はない。 これらのループ機能に加えて、プロパティの受け渡し(Props)、イベントハンドリング(Events)、条件分岐(Conditionals)といったSvelteの基本的な概念を理解すれば、どんなデータ量にも対応できる柔軟なUIを構築するための強固な基礎が身につく。