【ITニュース解説】Svelte Components Explained: Props & Composition Made Simple
2025年09月04日に「Dev.to」が公開したITニュース「Svelte Components Explained: Props & Composition Made Simple」について初心者にもわかりやすいように丁寧に解説しています。
ITニュース概要
Svelteはアプリを部品(コンポーネント)に分割し開発を効率化する。`$props()`で親からデータを受け取り、`$state()`でコンポーネント内のデータを管理、`$derived()`でそれらに基づく値を自動計算する。これにより、再利用性の高い構成でスムーズなアプリ構築が可能だ。
ITニュース解説
現代のウェブアプリケーションは、非常に複雑な機能を持ちながらも、ユーザーには滑らかで直感的に操作できる感覚を与える。例えば、Amazonの商品ページを開くと、商品の画像ギャラリー、顧客レビュー、おすすめ商品、支払いフォーム、そしてログイン画面など、多くの異なる要素が一つに統合されているように見える。また、FacebookのようなSNSでは、ニュースフィード、ストーリーズ、チャットウィンドウ、通知といった様々な機能がそれぞれ独立していながらも、全体として流れるような一つのアプリとして機能している。このような体験を実現しているのが「コンポーネント」という概念である。コンポーネントは、巨大で複雑なアプリケーションを、小さく再利用可能な部品に分割するための基本的な考え方である。それぞれのコンポーネントは特定の機能やUIの一部を担当し、それらを組み合わせていくことで、一つの完成されたアプリケーションを構築できる。
Svelteというフレームワークでは、このコンポーネントの概念が非常にシンプルに実装されている。Svelteにおいて、コンポーネントは単なる.svelteファイルとして定義される。特別な設定や複雑な記述は不要で、作成したファイルをインポートし、あたかもカスタムHTMLタグのようにアプリケーション内に配置するだけで利用できる。
Svelteには、コンポーネント内部でデータを扱うためのいくつかの特別な機能が用意されている。一つは$props()で、これは親コンポーネントから子コンポーネントへと渡される入力を定義するために使用される。次に$state()があり、これはコンポーネント自身が内部で管理し、そのコンポーネントのライフサイクル中に変化する可能性のあるローカルなデータを追跡するために使われる。そして$derived()は、他のデータ(プロップやステート)が変更されたときに、自動的に再計算される新しい値を定義するために使用される。これらの機能は、コンポーネントが互いに連携し、データを同期させ、アプリケーションが動的に機能するための基盤となる。
SvelteKitプロジェクトの基本的な構成として、再利用可能なコンポーネントは通常src/libフォルダに格納される。これらのコンポーネントは、メインページとなるsrc/routes/+page.svelteファイル内でインポートされ、実際に使用される。開発時には、ターミナルでnpm run devコマンドを実行し、プロジェクトをローカル環境で動かす。
.svelteファイルは、一般的に三つの主要なセクションで構成される。一つはJavaScriptロジックを記述する<script>ブロック、もう一つはコンポーネントの見た目を定義するHTMLマークアップ、そしてそのコンポーネント固有のスタイルを適用する<style>ブロックである。
コンポーネントがどのように連携するかを理解するため、簡単な例を挙げる。src/lib内にHeader.svelte、ProductList.svelte、Footer.svelteという三つのコンポーネントを作成する。Header.svelteはページのヘッダーとして「Welcome to My Store」という見出しを表示する。ProductList.svelteは、現在は静的なリンゴ、バナナ、サクランボのリストを表示し、Footer.svelteは著作権表示を含むフッターを表示する。
これらのコンポーネントを組み合わせてメインページを作るには、src/routes/+page.svelteファイル内で各コンポーネントをインポートする。例えば、<script>ブロック内でimport Header from '$lib/Header.svelte';のように記述する。$libはSvelteKitが提供するsrc/libフォルダへのショートカットパスである。その後、HTMLマークアップ部分で<Header />、<ProductList />、<Footer />のようにカスタムタグとして記述することで、それぞれのコンポーネントが指定された場所にレンダリングされ、一つの統合されたページが構成される。このように、各コンポーネントは自身の役割に集中しながらも、全体として協調して動作し、滑らかなユーザー体験を提供する。
コンポーネントが並んで機能するだけでなく、親コンポーネントから子コンポーネントへデータを渡す必要が頻繁に生じる。例えば、挨拶のコンポーネントには挨拶する相手の名前、ボタンのコンポーネントには表示するテキスト、画像のコンポーネントには表示する画像のパスが必要となる。このデータ受け渡しの仕組みが「プロップ(Props)」である。プロップは、コンポーネントへの外部からの入力値と考えることができる。これはHTMLの<img>タグにsrcやaltといった属性を指定するのと同様の概念である。開発者が作成するSvelteコンポーネントでも、同じように機能する。
Svelteの.svelteファイル内で、コンポーネントが受け取るプロップは$props()から値を取り出すことで指定される。最も簡単な例として、親からnameというプロップを受け取って挨拶する子コンポーネントを考える。
src/lib/Child.svelteの<script>ブロック内でlet { name } = $props();と記述すると、このコンポーネントが親からnameという名前の値を受け取ることをSvelteに伝える。$props()は親から渡されるすべての値の集合体であり、{ name }という構文でその中からnameという値を取り出して、コンポーネント内で使用可能にする。その後、HTMLマークアップで<p>Hello, {name}!</p>のようにname変数を利用する。
このChildコンポーネントをsrc/routes/+page.svelteで利用する際は、<Child name="Ada" />や<Child name="Alan" />のように、HTML属性と同じ形式でname属性に値を指定して渡す。ページがレンダリングされると、子コンポーネント内の{name}は親から渡された実際の値に置き換わり、「Hello, Ada!」や「Hello, Alan!」と表示される。
プロップにはデフォルト値を設定することも可能である。例えばlet { greeting = "Hello", name = "stranger" } = $props();と記述することで、親がgreetingやnameプロップを省略した場合に、代わりにデフォルト値が使われるようにできる。これはオプションの値を扱う際に非常に便利である。
プロップとして渡せるのは静的な文字列だけでなく、JavaScriptのあらゆる値、例えば数値、オブジェクト、配列、さらには関数も可能である。親コンポーネントでlet user = { name: "Grace", age: 36 };のようなオブジェクトを定義し、<Child user={user} />のように渡すことができる。子コンポーネントではlet { user } = $props();で受け取り、<p>{user.name} is {user.age} years old.</p>のようにオブジェクトのプロパティを利用できる。プロップ名と渡す変数名が同じ場合、user={user}を短縮して<Child {user} />と記述する便利な記法も存在する。このようにプロップは、親が子にデータを渡すための明確で柔軟な手段を提供する。
プロップが親から与えられる外部データであるのに対し、「ステート(State)」はコンポーネント自身が所有し、内部で管理するデータである。ステートは、ユーザーの操作やコンポーネント内のロジックによって変化し、それに伴ってUIも更新される。プロップが外部から供給される材料だとすれば、ステートはコンポーネント自身の内部で調理される材料と考えることができる。
ステートの具体的な例として、クリックするたびにカウントが増加するカウンターコンポーネントを挙げる。
src/lib/Counter.svelteでは、<script>ブロック内でlet count = $state(0);と記述する。ここで$state(0)は、countがこのコンポーネントの内部状態であり、初期値が0であることを示す。そしてHTMLマークアップで<button on:click={() => count++}>Count: {count}</button>のようにボタンとカウント表示を定義する。このボタンがクリックされるたびにcountの値が増加し、SvelteによってUIが自動的に更新される。これがステートの基本的な動作である。
プロップとステートを組み合わせて使うことも可能である。例えば、親から初期値を受け取り、その値からカウントを開始するが、その後のカウントはコンポーネント自身で管理・更新できるカウンターを考えてみる。
src/lib/StartCounter.svelteでは、まずlet { initial } = $props();で親からinitialプロップを受け取る。次に、このinitial値を初期値として、let count = $state(initial);のようにコンポーネントのローカルなステートを設定する。これにより、コンポーネントは親から提供された初期値を出発点として自身のカウントを開始し、その後のカウントは自身で独立して管理する。
src/routes/+page.svelteでは、<StartCounter initial={0} />や<StartCounter initial={10} />のように、それぞれのカウンターコンポーネントに異なる初期値を渡すことができる。各StartCounterコンポーネントは、親から受け取ったinitial値に基づいて独自のcountステートを持ち、ボタンをクリックしても他のカウンターには影響を与えず、独立してカウントを更新する。
ここで重要な注意点として、プロップは親から子への読み取り専用の入力であり、子コンポーネント内でプロップの値を直接変更しようとしてはいけない。例えば、initial++;のようにプロップを直接インクリメントしようとしても、期待通りには機能しない。もしプロップとして受け取った値を変更したい場合は、先ほどの例のように、その値を一度$state()で宣言したローカルなステートにコピーし、そのステートを操作する必要がある。つまり、外部から来る値には$props()を使い、コンポーネント内部で制御・変更する値には$state()を使うのが原則となる。
プロップによるデータ受け渡しとステートによる内部データ管理を理解したら、次に「コンポジション」という概念が重要になる。コンポジションとは、複数のコンポーネントを組み合わせて、より大きなUI部品やページ全体を構築するプロセスを指す。これは、ヘッダー、商品リスト、フッターを組み合わせて完全なページを作る大規模なものから、ボタンのような小さな再利用可能な要素、あるいは独自のヘッダーとボディを持つカードのようなネストされたコンポーネントの作成といった、より小さなスケールでも適用される。
再利用可能なボタンの例を考えてみる。
src/lib/Button.svelteでは、let { label = "Click me" } = $props();でlabelプロップを受け取り、デフォルト値も設定する。このlabelを表示するボタンのHTMLマークアップを定義する。
src/routes/+page.svelteでこのButtonコンポーネントをインポートし、<Button label="Save" />や<Button label="Cancel" />のように、異なるlabelプロップを渡して複数回使用する。これにより、一度ボタンを作成するだけで、アプリケーション内の様々な場所で異なるテキストを持つボタンを容易に配置できる。もし後でボタンのスタイルや機能を変更した場合でも、すべての使用箇所で自動的に更新されるため、コードの保守性が向上する。これが、コンポジションの再利用可能な側面である。
次に、ネストされたコンポーネントによるコンポジションの例として、カードコンポーネントを考える。このカードをCardHeaderとCardBodyという、より小さな部品に分割し、それらをCardコンポーネント内で組み合わせる。
src/lib/CardHeader.svelteはtitleプロップを受け取り、そのタイトルを<h2>タグで表示する。src/lib/CardBody.svelteはcontentプロップを受け取り、その内容を<p>タグで表示する。
そして、src/lib/Card.svelteでは、<script>ブロックでCardHeaderとCardBodyをインポートし、さらに親からtitleとcontentプロップを受け取る。その上で、HTMLマークアップで<div class="card">の中に<CardHeader {title} />と<CardBody {content} />をネストして配置する。このように、Cardコンポーネントは、さらに小さなコンポーネントを組み合わせて作られている。
最後に、src/routes/+page.svelteでこのCardコンポーネントをインポートし、<Card title="Banana" content="A yellow fruit that's great in smoothies." />のように、異なるtitleとcontentプロップを渡して利用する。このようにコンポジションは、小さな部品が協力してより大きな部品を形成し、それがさらに大きな構造の一部となる、階層的な構築を可能にする。これは、アプリケーションを管理しやすくし、再利用性を高める上で非常に重要な概念である。
プロップやステートによってデータを受け渡し、内部データを管理する方法を理解したが、これらのデータから新しい値を計算し、その計算結果が元のデータの変更に合わせて常に最新の状態に保たれるようにしたい場合がある。このような目的のためにSvelteには$derived()という機能が提供されている。
プロップが更新される仕組みについて注意点がある。親コンポーネントが子コンポーネントに渡すプロップの値を変更しても、子コンポーネント全体が再構築されるわけではない。Svelteは、子コンポーネント内の該当するプロップ変数のみを更新する。これは、もしlet { discount } = $props();のようにプロップを受け取っていた場合、discount変数は親の変更に応じて更新されることを意味する。しかし、let discountedPrice = price - (price * discount);のようにdiscountedPriceを単純な変数として計算していた場合、この計算はコンポーネントが最初に作成された時に一度実行されるだけで、その後priceやdiscountが変更されても自動的に再計算されることはない。
この問題を解決し、計算された値が常に最新の状態に保たれるようにするのが$derived()の役割である。$derived()はSvelteに対し、「この計算は、その入力データが変更されるたびに同期を保つべきである」と指示する。
具体的な例として、商品の元値と割引後の価格を表示するプロダクトカードを考えてみる。
src/lib/ProductCard.svelteでは、まずlet { price, discount } = $props();でpriceとdiscountという二つのプロップを受け取る。次に、割引後の価格を計算するためにlet discountedPrice = $derived(price - (price * discount));と記述する。ここで$derived()を使うことで、priceまたはdiscountの値が変更されるたびに、discountedPriceが自動的に再計算されるようになる。そして、HTMLマークアップで元の価格と割引後の価格を表示する。
src/routes/+page.svelteでは、このProductCardをインポートし、let price = 100;とlet discount = $state(0.1);という変数を用意し、<ProductCard {price} {discount} />で子コンポーネントに渡す。さらに、applyCouponという関数でdiscountの値を0.25に変更するボタンを用意する。
最初は、元値100ドル、割引率10%なので、割引後の価格は90ドルと表示される。ここで親コンポーネントの「Apply Coupon」ボタンをクリックすると、discountの値が0.25に更新される。discountedPriceは$derived()で宣言されているため、子コンポーネント内で自動的に再計算が実行され、割引後の価格は75ドルに更新され、UIもそれに合わせて表示が変わる。
もしdiscountedPriceを$derived()を使わずに単純な変数として宣言していた場合、discountの値が変更されてもdiscountedPriceは最初に計算された90ドルのまま更新されず、UIも90ドルのままであった。このように$derived()は、計算された値が入力の変更に追随して更新されるようにするための不可欠な機能である。
まとめると、$props()は親からの外部入力、$state()はコンポーネント自身の内部データ、そして$derived()は、それらのソースデータが変更されたときに自動的に更新される計算値のために使用する。
コンポーネントを効果的に構築する上で、プロップの利用に関して初心者が陥りやすいいくつかの注意点がある。これらを事前に理解しておくことで、開発時の問題を回避できる。
一つ目の注意点は、$props()の使用を忘れてしまうことである。単に<script> let name; </script>と記述しただけでは、たとえ親がname="Ada"のように値を渡しても、子コンポーネントはそれを受け取ることができない。プロップとして機能させるためには、必ずlet { name } = $props();のように$props()から値を取り出す必要がある。$props()は、親から渡されたすべての値が格納されている「バスケット」のようなもので、そのバスケットに手を伸ばさなければ、コンポーネントは何も受け取れない。
二つ目の注意点は、JavaScriptの予約語をプロップ名に使用できないことである。class、for、letなどの単語はJavaScriptに特別な意味があるため、プロップ名として直接使うことはできない。例えば、<Child class="highlighted" />のようにclassをプロップ名として使うと、期待通りに機能しない可能性がある。このような場合は、cssClassのように、少し異なる名前に変更する必要がある。もしプロップ名に迷った際は、予約語でない別の名前を選ぶのが安全である。
三つ目の注意点は、プロップは常に一方向の流れであるという原則である。プロップは親から子へと一方通行で流れる。親がプロップの値を変更すれば、子コンポーネントはそれに合わせて更新される。しかし、子コンポーネントが親のプロップの値を直接変更することはできない。例えば、親がcountという$state()変数を持つ場合、それを<Child value={count} />として子に渡すことができる。子コンポーネントは<p>Child sees: {value}</p>としてそのvalueを表示できる。親のボタンをクリックしてcountを増やせば、子は更新されたvalueを見ることができる。しかし、子が自身の内部でvalue++のような操作をしても、親のcountを直接更新することはできない。プロップは入力であり、双方向の接続ではない。子が親に何かを伝えたい場合は、イベントやバインディングといった別の仕組みを使う必要がある。
これまでの解説をまとめると、Svelteにおけるコンポーネントはアプリケーションを構成する基本的な部品である。$props()は親から子へデータを渡すための仕組みであり、デフォルト値や動的な値、オブジェクトなども渡すことができる。$state()はコンポーネント自身が内部で管理するデータのために用いられ、その変化がUIに反映される。$derived()は、プロップやステートなどの他のデータに基づいて計算される新しい値を定義し、それらのソースデータが変更されたときに自動的に更新される。コンポジションは、小さなコンポーネントを組み合わせてより大きなUI部品やページ全体を構築する技術であり、再利用性と構造化を促進する。プロップは親から子への一方向の流れであるという原則を理解し、$props()の宣言忘れや予約語の使用といった一般的な注意点を避けることで、Svelteを使ったアプリケーション開発の強固な基盤を築くことができる。
現在のところ、コンポーネントは親からプロップを受け取るという形で連携しているが、実際のアプリケーションでは、子コンポーネントが親コンポーネントに何らかのアクションを通知したり、双方向でデータを同期したりする必要がある。次の段階では、Svelteにおける双方向のコミュニケーションを可能にする「イベント」と「バインディング」について深く学ぶことになる。