本文書はこれまでSvelteを利用した経験がない人を対象にした入門的な文書です。Svelteのすべての機能の説明を行なっているわけではありませんが利用頻度の高そうな機能を中心にシンプルなコードを利用して解説を行なっているので本文書を読み進めるとSvelteの基本的な機能を理解することができます。

ライブラリの追加を行うことなく利用できるTransition、Animationやwritable storeによるコンポーネント間のデータの共有(状態管理)の設定についても解説しています。

目次

Svelteについて

SvelteはWEBアプリケーションのUI(ユーザインタフェイス)を効率的に開発するためのJavaScriptフレームワークです。JavaScriptのフレームワークと言えば最近ではReact, Vueと並んで紹介されるほど人気のあるフレームワークになっています。

ではSvelteとReact, Vueとの違いはなんでしょう?主にReactやVueと比較される際に2つの違いがあげられます。

1つ目はコンパイラです。Svelteのルールに沿って記述したsvelteファイル(1つのファイルの中でJavScript, HTML, CSSの3つで構成されている)はビルド時にコンパイルが行われ、Vanilla JavaScriptのコード(通常のJavaScriptコード)とCSSに変換されます。クライアント側ではコンパイルされたJavaScriptとCSSのコードを利用してレンダリングが行われるためフレームワークのコードをバンドルする必要がありません。その結果、ファイルサイズが小さく、ファイルのロード時間もかからないため高速に動作します。

2つ目はSvelteではVirtual DOM(仮想DOM)を利用しない点です。Virtual DOMでは実際のDOMとは別に変数の更新を行う際(状態の更新)に更新前と更新後のVirtual DOMを作成します。作成した2つの仮想DOMの差分を取り、差分のみ実際のDOMに適用します。そのためVirtual DOMの作成、差分を取る処理にオーバーヘッドが発生します。SvelteではVirtial DOMを利用しないので更新が行われる可能性がある変数を監視し、変数の更新が行われると実際のDOMを更新します。その結果、Virtual DOMを利用するよりも高速に動作します。

他のフレームワーク/ライブラリの比較とは別にSvelteのドキュメントでは特徴として3つを挙げています。”Write less code”, “No Virtual DOM”, “Truly reactive”。React, Vue.jsでコードを記述した経験がある人であれば一つ目の”Write less code”について本文書を通して実感してもらえると思います。

SvelteはUIを作成するために利用しますがReactにNext.jsやRemix, VueにNuxtがあるようにSvelteではSvelteKitを利用することでフルスタックアプリケーションを効率よく構築することができます。

SvelteKitについては下記の文書で公開しています。

Svelteのインストール

※以前は下記の手順がドキュメントに記載されていましたが今はローカルにSvelteをインストールする際にはSvetekitをインストールすることを推奨しています(npm create svelte@latest myapp)。Svelte単体であればViteを利用してインストールすることを推奨しています。

Svelteのインストールはnpx degitコマンドを利用して行うことができます。svelte_projectは任意の名前なので好きなプロジェクト名を設定してください。コマンドを実行するとsvelte_projectが作成され必要なファイルがダウンロードされています。


 % npx degit sveltejs/template svelte_project
Need to install the following packages:
  degit
Ok to proceed? (y) 
> cloned sveltejs/template#HEAD to svelte_project
sveltejs/templateは現在はメンテンスされていません。

コマンドの実行が完了したら作成されるフォルダに移動してnpm installコマンドを実行します。package.jsonに記載されているJavaScriptパッケージのインストールが行われます。


 % npm install

パッケージのインストール完了後、npm run devコマンドを実行して開発サーバの起動を行います。


 % npm run dev

> svelte-app@1.0.0 dev
> rollup -c -w

rollup v2.78.1
bundles src/main.js → public/build/bundle.js...
LiveReload enabled
created public/build/bundle.js in 272ms

[2022-08-23 22:53:09] waiting for changes...

> svelte-app@1.0.0 start
> sirv public --no-clear "--dev"


  Your application is ready~! 🚀

  - Local:      http://localhost:8080
  - Network:    Add `--host` to expose

デフォルトではポート8080で起動することが確認できるのでブラウザでアクセスしてください。下記の画面が表示されます。

Svelteの初期画面
Svelteの初期画面

Viteによるインストール

本文書ではnpx digitを利用して作成したプロジェクトで動作確認を行っていますがViteを利用することができます。Viteを利用したインストール方法を確認しておきます。

npm create vite@latestコマンドを実行するとプロジェクト名の入力とフレームワークの選択を行う必要があります。プロジェクト名は任意の名前をつけることができここではsvelte-vite-projectとしています。

フレームワークはReactやVueなど選択できますがsvelteまたはsvelte-tsを選択してください。


 % npm create vite@latest
✔ Project name: … svelte-vite-project
✔ Select a framework: › svelte
✔ Select a variant: › JavaScript

Scaffolding project in /Users/mac/Desktop/svelte-vite-project...

Done. Now run:

  cd svelte-vite-project
  npm install
  npm run dev

— — template svelteをつけて実行するとsvelteの選択を行わずプロジェクトの作成を行うことができます。


 % npm create vite@latest myapp -- --template svelte
Scaffolding project in /Users/mac/Desktop/svelte_project...

Done. Now run:

  cd svelte_project
  npm install
  npm run dev

プロジェクトフォルダの作成が完了したら作成したフォルダに移動してnpm installを実行後、npm run devコマンドを実行すると開発サーバが起動します。


% cd svelte-vite-project 
% npm install
% npm run dev

  VITE v3.0.9  ready in 432 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose

ブラウザからhttp://127.0.0.1:5173/にアクセスすると初期画面が表示されます。

svelte vite初期画面
svelte vite初期画面

ブラウザに表示されるまでの流れ

sveltejs/templateの場合

ブラウザに表示されている内容がどのように表示されているか確認するためにsrcフォルダのmain.jsファイルを確認します。main.jsはアプリケーションのエントリーポイントファイルでサーバにアクセスした際に必ず実行されるJavaScriptファイルです。


import App from './App.svelte';

const app = new App({
  target: document.body,
  props: {
    name: 'world'
  }
});

export default app;

main.jsファイルの中ではsrcフォルダにあるApp.svelteファイルをimportし、importしたルートコンポーネントAppにはtargetとporpsプロパティを持つオブジェクトを引数に設定してappインスタンスを作成しています。propsはpropertiesの略でコンポーネントから子コンポーネントにデータを渡す時に利用することができます。ここではnameという名前で”world”という文字列を渡しています。targetにはどのDOMに対してappをマウントするのかを指定します。targetにはpublicフォルダにあるindex.htmlのbodyタグを指定しているためbodyタグの中にSvelteが作成するコンテンツが挿入されることになります。


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset='utf-8'>
  <meta name='viewport' content='width=device-width,initial-scale=1'>

  <title>Svelte app</title>

  <link rel='icon' type='image/png' href='/favicon.png'>
  <link rel='stylesheet' href='/global.css'>
  <link rel='stylesheet' href='/build/bundle.css'>

  <script defer src='/build/bundle.js'></script>
</head>

<body> 
//ここにSvelteを使って作成したコンテンツが挿入される
</body>
</html>
taregetで指定する要素は必ずしもbodyタグである必要はありません。div要素にid=”app”を設定しそのidをtargetでdocument.getElementById(‘app’)で設定するとdiv要素の間にコンテンツが挿入されます。

linkのhref, scriptタグのsrcには/buildフォルダが指定されています。これはプロジェクトフォルダのpublicフォルダのbuildを指定しています。buildフォルダはnpm run devコマンドを実行すると作成され、コンパイルされたJavaSciptファイルとcssファイルが保存されます。

main.jsでimportされていたApp.svelteファイルを確認するとブラウザ上に表示されていた内容が記述されていることがわかります。{name}はpropsで渡されていたnameの値である”world”が入ることになります。


<script>
  export let name;
</script>

<main>
  <h1>Hello {name}!</h1>
  <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
</main>

<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  h1 {
    color: #ff3e00;
    text-transform: uppercase;
    font-size: 4em;
    font-weight: 100;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

SvelteもVue, Reactと同様にコンポーネントを利用してアプリケーションを作成していきます。コンポーネントファイルにはSvelteの拡張子である.svelteがつきます。svelteファイルはSingle File Components(SFC)で1つのファイルの中に3つのパート(script, html, style)に分けコードを記述することができます。scriptにはJavaScriptコード、htmlにはHTMLのマークアップ、styleにはCSSを記述します。すべてのパートは必須ではないので必要なもののみ記述します。

index.htmlのbodyタグにSvelteで作成したコンテンツが挿入されているかはブラウザのデベロッパーツールを利用して確認することができます。i

bodyタグの中に挿入されるコンテンツの確認
bodyタグの中に挿入されるコンテンツの確認

ブラウザがindex.htmlをサーバから受け取った時はbodyタグの中は空ですが、index.htmlにリンクが貼られているJavaScriptファイル(bundle.js)をダウンロードし実行することでブラウザ側でコンテンツを作成してbodyタグに挿入することでブラウザ上にコンテンツが表示されます。

例えばVue.jsの場合はHTMLのコンテンツはtemplateタグの中に記述しますがSvelteではhtmlタグは必要ありません。

このようにブラウザ側(クライアント)でJavaScriptを利用してコンテンツを表示することから一般的にCSR(Client Side Rendering)と呼びます。bodyタグの中身はすべてJavaScriptファイルをブラウザ側で実行することで表示されているということを理解しておく必要があります。

Viteの場合

ブラウザに表示されている内容がどのように表示されているか確認するためにsrcフォルダのmain.jsファイルを確認します。main.jsはアプリケーションのエントリーポイントファイルでindex.htmlファイルのscriptタグに設定されているためサーバにアクセスした際に必ず実行されるJavaScriptファイルです。


import './app.css'
import App from './App.svelte'

const app = new App({
  target: document.getElementById('app'),
})

export default app

main.jsファイルの中ではsrcフォルダにあるApp.svelteファイルをimportしています。Appのインスタンス作成時の引数にはtargeでどのDOMに対してSvelteインスタンスをマウントするのかを指定しています。targetにはプロジェクトフォルダ直下のindex.htmlのidにappを持つをdivタグ指定しているためdivタグの中にSvelteが作成するコンテンツが挿入されることになります。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Svelte</title>
  </head>
  <body>
    <div id="app">
//ここにSvelteを使って作成したコンテンツが挿入される
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

scriptタグのsrcはsrcフォルダのmain.jsファイルが指定されています。

main.jsでimportされていたApp.svelteファイルを確認するとブラウザ上に表示されていた内容が記述されていることがわかります。


<script>
  import svelteLogo from './assets/svelte.svg'
  import Counter from './lib/Counter.svelte'
</script>

<main>
  <div>
    <a href="https://vitejs.dev" target="_blank" rel="noreferrer"> 
      <img src="/vite.svg" class="logo" alt="Vite Logo" />
    </a>
    <a href="https://svelte.dev" target="_blank" rel="noreferrer"> 
      <img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
    </a>
  </div>
  <h1>Vite + Svelte</h1>

  <div class="card">
    <Counter />
  </div>

  <p>
    Check out <a href="https://github.com/sveltejs/kit#readme" target="_blank" rel="noreferrer">SvelteKit</a>, the official Svelte app framework powered by Vite!
  </p>

  <p class="read-the-docs">
    Click on the Vite and Svelte logos to learn more
  </p>
</main>

<style>
  .logo {
    height: 6em;
    padding: 1.5em;
    will-change: filter;
    transition: filter 300ms;
  }
  .logo:hover {
    filter: drop-shadow(0 0 2em #646cffaa);
  }
  .logo.svelte:hover {
    filter: drop-shadow(0 0 2em #ff3e00aa);
  }
  .read-the-docs {
    color: #888;
  }
</style>

SvelteもVue, Reactと同様にコンポーネントを利用してアプリケーションを作成していきます。コンポーネントファイルにはSvelteの拡張子である.svelteがつきます。svelteファイルはSingle File Components(SFC)で1つのファイルの中に3つのパート(script, html, style)に分けコードを記述することができます。scriptにはJavaScriptコード、htmlにはHTMLのマークアップ、styleにはCSSを記述します。すべてのパートは必須ではないので必要なもののみ記述します。JavaScriptコードはscriptタグ、CSSはstyleタグの中に記述しますがHTMLはタグで囲む必要はありません。Vue.jsであればHTMLはtemplateタグで囲みます。

index.htmlのbodyタグにSvelteで作成したコンテンツが挿入されているかはブラウザのデベロッパーツールを利用して確認することができます。

div要素の中に挿入されるコンテンツの確認
div要素の中に挿入されるコンテンツの確認

ブラウザがindex.htmlをサーバから受け取った時はdivの中は空ですが、index.htmlにリンクが貼られているJavaScriptファイル(main.js)をダウンロードし実行することでブラウザ側でコンテンツを作成してdivタグに挿入することでブラウザ上にコンテンツが表示されます。

ブラウザが受け取るindex.htmlの中身
ブラウザが受け取るindex.htmlの中身

このようにブラウザ側(クライアント)でJavaScriptを利用してコンテンツを表示することから一般的にCSR(Client Side Rendering)と呼びます。

VSCodeのExtensions

エディターにVSCodeを利用している場合はExtensionsにSvelte for VS Codeを利用することでコードがハイライトされます。

Svelteの基礎

動作確認はsveltejs/templateを利用してインストールした環境で行なっているためViteで同様の動作確認を行う場合はmain.jsファイルとApp.svelteファイルを以下のように書き換えてください。


import './app.css';
import App from './App.svelte';

const app = new App({
  target: document.getElementById('app'),
  props: {
    name: 'world',
  },
});

export default app;

<script>
  export let name;
</script>

<main>
  <h1>Hello {name}!</h1>
  <p>
    Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn
    how to build Svelte apps.
  </p>
</main>

<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  h1 {
    color: #ff3e00;
    text-transform: uppercase;
    font-size: 4em;
    font-weight: 100;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

コンポーネント

アプリケーションの画面はヘッダーやフッターなど機能、役割毎に分割することができます。分割した機能/役割毎にコンポーネント化することでコンポーネント毎に開発、管理を行うことができます。Svelteではコンポーネントを利用して開発を行います。

デフォルトではmain.jsファイルからAppコンポーネントをimportしているように別ファイルの中でコンポーネントを定義してあるコンポーネントから別のコンポーネントをimportして利用することができます。

srcフォルダにCounter.svelteファイルを作成します。拡張子は.svelteでHTMLのマークアップのみ記述しています。


<h2>カウンターコンポーネント</h2>
一般的にcomponentsやlibフォルダをsrcフォルダの直下に作成してその中にコンポーネントファイルを作成します。viteを利用した場合はlibフォルダにコンポーネントが作成されています。

App.svelteの中でCounter.svelteファイルをimportします。


<script>
  import Counter from './Counter.svelte';
  export let name;
</script>

<main>
  <h1>Hello {name}!</h1>
  <Counter />
</main>

<style>
//略

別ファイルとして作成したファイルをimportすることでファイルの内容をブラウザ上に表示することができます。

カウンターコンポーネントを表示
カウンターコンポーネントを表示

importしたCounterコンポーネントファイルはAppコンポーネント内で何度でも利用することができます。


<main>
  <h1>Hello {name}!</h1>
  <Counter />
  <Counter />
  <Counter />
</main>
importしたコンポーネントも複数回利用
importしたコンポーネントも複数回利用

Counter.svelteファイルはAppコンポーネントに限らず別のコンポーネントからもimportすることで再利用することができます。

propsの理解

propsはpropertiesの略であるコンポーネントから子コンポーネントにデータを渡したい場合に利用することができます。先ほど作成したCounterコンポーネントはAppコンポーネントでimportされ利用されているのでAppコンポーネントの子コンポーネントになります。そのためpropsを利用してデータを渡すことができます。

Appから文字列を渡したい場合には下記のようにCounterタグにname属性を記述することができます。nameという名前のpropsで”カウンター”という文字列を渡しています。


<Counter name="カウンター" />

propsを受け取るCounterコンポーネント側ではscriptタグを追加してexportを利用して変数nameを定義します。定義したnameはカーリーブレースで囲むことでブラウザ上に表示させることができます。propsで受け取る場合はexportが必須なので注意してください。exportを設定していない場合にはpropsとして値を受け取ることができないためnameの値は”undefined”となります。


<script>
  export let name;
</script>

<h2>{name}コンポーネント</h2>
カウンターコンポーネントを表示
propsで渡されたnameの中身を表示

Counterタグにpropsを設定していない場合は初期値としてundefinedが設定されます。


<Counter >
propsの初期値はundefined
propsの初期値はundefined

Counterコンポーネントで初期値を設定することもできます。AppコンポーネントのCounterタグにname propsを設定していない場合には初期値が利用されます。


<script>
  export let name = 'Counter';
</script>

<h2>{name}コンポーネント</h2>
propsに設定した初期値を表示
propsに設定した初期値を表示

変数の定義

propsで渡された変数をブラウザ上に表示することができましたがコンポーネント内でのみ利用する変数を定義することもできます。count変数を定義して値を0としています。propsと同様にカーリーブレースを利用してブラウザ上に表示させることができます。値の更新が行われることを想定してletを利用していますがconstを利用することもできます。


<script>
  export let name = 'Counter';
  let count = 0;
</script>

<h2>{name}コンポーネント</h2>
<p>
  カウント:{count}
</p>
定義した変数の値を表示
定義した変数の値を表示

Reactiveな変数+clickイベント

コンポーネントで定義した変数はユーザとのインタラクションで動的に変更を行うことができます。Svetelでは定義した変数をReactiveな変数として扱うために追加設定は必要ないため先ほど定義したcountをそのまま利用することができます。

ユーザがボタンをクリックするとカウンターの数が増えるようにボタンにはclickイベントを設定します。on:ディレクティブの後にイベント名を設定することでイベントを設定することができ、クリックイベントの場合はon:clickとなります。on:clickの後にはクリックした場合に実行される関数を指定します。ここではcountの数を増やすためincrement関数を設定してscriptタグの中にincrement関数を定義します。


<script>
  export let name = 'Counter';
  let count = 0;
  const increment = () => {
    count += 1;
  };
</script>

<h2>{name}コンポーネント</h2>
<p>
  カウント:{count}
</p>
<div>
  <button on:click={increment}>Up</button>
</div>

ページを開いた直後ではcountにはデフォルト値の0が表示されていますが”Up”ボタンをクリックすると値が1ずつ増えていきます。ユーザとのインタラクションを持つUIを作成することができました。

Reactivityの動作確認
Reactivityの動作確認

increment関数を定義しましたがon:clickにインラインで処理を記述することもできます。


<script>
  export let name = 'Counter';
  let count = 0;
</script>

<h2>{name}コンポーネント</h2>
<p>
  カウント:{count}
</p>
<div>
  <button on:click={() => (count += 1)}>Up</button>
</div>

Reactive Statement

新たにcountの数を2倍で表示するdouble変数を定義します。count値が0や1だとcountとdoubleの値が一緒になってしまうためcountのデフォルト値は2としています。


<script>
  export let name = 'Counter';
  let count = 2;
  const increment = () => {
    count += 1;
  };
  let double = count * 2;
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{count}</p>
  <p>ダブルカウント:{double}</p>
</div>
<div>
  <button on:click={increment}>Up</button>
</div>

ページを開いた直後はダブルカウントは4と表示されていますが”Up”ボタンをクリックするとcountの数は増えますがdoubleの数に変化はありません。

変数doubleの値に変化なし
変数doubleの値に変化なし

現在の設定ではdoubleの値はcountの更新の影響を受けませんがcountの値が更新するとdoubleの値も一緒に更新させることができます。doubleの変数の前に$:マークを追加します。$:を設定することでcountの値が更新されるとdoubleの値も一緒に更新されるようになります。


$: double = count * 2;
doubleの値がcountと一緒に更新
doubleの値がcountと一緒に更新

変数だけではなく$:にはconsole.logも設定することができます。ブラウザを表示した直後メッセージが1度表示され後はボタンをクリックする度にコンソールにメッセージが表示されます。console.logの中に更新を行うリアクティブな変数countが含まれている必要があります。countが含まれていない場合はブラウザを表示した直後の1だけメッセージが表示されボタンをクリックしても何も表示されません。


$: console.log(`現在のcountの値は${count}です`);

複数のconsole.logも下記のように表示させることができます。この場合もリアクティブな変数countが含まれている必要があります。


$: {
  console.log(`現在のcountの値は${count}です`);
  console.log('複数行も設定できます。');
}

svelte/storeモジュール

svelte/storeモジュールを利用することでReactiveを実現することができます。後ほど説明を行いますがsvelte/storeモジュールを利用することでコンポーネント間でのデータの共有を行うことができます。ここではsvelte/storeモジュールを利用してどのようにReactiveが実現できるのか動作確認します。

svelte/storeモジュールの中にには読み書きを行うwritableの他に読み込みだけを行え外部から更新できないreadable, 他のstoreを利用するderived, 値を取得するgetなどがあります。

writable

writableを利用するためにはsvelte/storeからwritebleをimportします。writable関数では引数に初期値を設定します。countにwritable関数の引数に0を設定して定義していますがwritable関数の引数にはオブジェクトを設定することもできます。writable関数を利用して定義したcountは3つの関数を持ちます。set, update, subscribeでcountが参照する値の設定にset, 更新を行う場合にはupdate, subscribeでは更新が行われると通知が行われるので通知を利用して値の更新を検知することができます。

writableの戻り値に含まれる3つの関数
writableの戻り値に含まれる3つの関数

実際にカウンターをwritable関数を利用して記述すると下記のようになります。countの前に$をつけることでwriteble関数で設定した値を取得することができます。updateメソッドのcallback関数の引数には現在の値を持っているのでクリックイベントでincrement関数が実行されると現在の値に1を足して新たな値となります。


<script>
  import { writable } from 'svelte/store';
  export let name = 'Counter';
  const count = writable(0);
  const increment = () => {
    count.update((value) => value + 1);
  };
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{$count}</p>
</div>
<div>
  <button on:click={increment}>Up</button>
</div>

subscribeの設定

$ではなくsubscribeメソッドを利用した場合には下記のように記述することができます。先ほどのように$を利用するのではなく別にReactiveな変数countValueを定義します。subscribeを利用することで値が更新されると更新した値をnotificationとして受け取るができるので受け取った値をcountValueに保存して表示させています。


<script>
  import { writable } from 'svelte/store';
  export let name = 'Counter';
  let countValue;

  const count = writable(0);
  count.subscribe((value) => {
    countValue = value;
  });
  const increment = () => {
    count.update((value) => value + 1);
  };
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{countValue}</p>
</div>
<div>
  <button on:click={increment}>Up</button>
</div>

writableの第二引数の設定

writable関数の引数は初期値だけではなく第二引数にcallback関数を設定することができます。しかし実行されるのはsubscriberが設定されている場合のみです。callback関数の引数にはset関数を持ちます。callback関数の中で必ずset関数を利用する必要はありません。

下記のようにwritable関数に第2引数を設定してincrement関数を設定しますがブラウザ上には値は表示させません(この状況がsubscriberが設定されていない状態)。


<script>
  import { writable } from 'svelte/store';

  export let name = 'Counter';
  // let countValue;

  const count = writable(0, (set) => {
    set(100);
  });
  // count.subscribe((value) => {
  //   countValue = value;
  // });
  const increment = () => {
    count.update((value) => {
      console.log(value);
      return value + 1;
    });
  };
</script>

<h2>{name}コンポーネント</h2>
<!-- <div>
  <p>カウント:{countValue}</p>
</div> -->
<div>
  <button on:click={increment}>Up</button>
</div>

この状態でUpボタンをクリックするとブラウザのコンソールには0, 1, 2, ….と値が増えてきます。このようにsubscriberが設定されていない状況では第二引数に設定した関数は実行されません。

次にsubscribeメソッドの設定を行いsubscriberrを追加します。画面にcountValueを表示させるかどうかに関わらず第二引数のset関数が実行されコンソールにはボタンをクリックすると100, 101, 102…と増えていきます。


<script>
  import { writable } from 'svelte/store';

  export let name = 'Counter';
  let countValue;

  const count = writable(0, (set) => {
    set(100);
  });
  count.subscribe((value) => {
    countValue = value;
  });
  const increment = () => {
    count.update((value) => {
      console.log(value);
      return value + 1;
    });
  };
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{countValue}</p>
</div>
<div>
  <button on:click={increment}>Up</button>
</div>

$countを設定した場合もsubscriberとしてカウントされ第二引数のset関数が実行されます。


<script>
  import { writable } from 'svelte/store';

  export let name = 'Counter';

  const count = writable(0, (set) => {
    set(100);
  });
  const increment = () => {
    count.update((value) => {
      console.log(value);
      return value + 1;
    });
  };
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{$count}</p>
</div>
<div>
  <button on:click={increment}>Up</button>
</div>

subscribeを実行すると戻り値としてunsubscribe関数が戻されます。通知設定をunsubscribe関数で解除することができます。

unsubscribe関数

subscribeを実行すると戻されるunsubscribe関数を実行します。


const count = writable(0);
const unsubscribe = count.subscribe((value) => {
  countValue = value;
});
const increment = () => {
  count.update((value) => {
    console.log(value);
    return value + 1;
  });
};
unsubscribe();

unsubscribeが実行されているため画面上のボタンをクリックしてもcountValueの値が更新されることはありませんがブラウザのコンソールにはボタンをクリックする度に値が増えていきます。

unsubscribe関数はライフサイクルフックのonDestoryに設定することでコンポーネントのアンマウント時にのみ実行させることができます。


import { writable } from 'svelte/store';
import { onDestroy } from 'svelte';
export let name = 'Counter';
let countValue;

const count = writable(0);
const unsubscribe = count.subscribe((value) => {
  countValue = value;
});
const increment = () => {
  count.update((value) => {
    console.log(value);
    return value + 1;
  });
};
onDestroy(() => {
  unsubscribe();
});

unsubscribe関数が実行されたかどうか知りたい場合にはwritableの第二引数を利用することができます。第二引数でreturnに関数を設定します。ここではunsubscribe関数が実行されるとコンソールにメッセージが表示されるようにしています。


const count = writable(0, () => {
  return () => {
    console.log('unsubscribe execute');
  };
});

先ほど設定したonDestory関数をコメントするとunsubscribe関数が実行されるのでブラウザのコンソールに”unsubscribe execute”が表示されます。


  // onDestroy(() => {
  unsubscribe();
  // });

readable

svelte/storeモジュールの中に含まれるreadableについて確認を行います。writableはset、updateメソッドを利用して値を更新することができますがreadableはsubscribeメソッドしか持っていないため外部から値を更新することができせん。

Counter.svelteファイルに以下のコードを記述します。


<script>
  import { readable, writable } from 'svelte/store';

  const time = readable(0);
  const count = writable(0);
  console.log('readable', time);
  console.log('writable', count);
</script>

<h2>{name}コンポーネント</h2>

ブラウザのコンソールでreadableとwriableから戻された値が持つ関数を確認することができます。readableはsubscribe, writableはset, subscribe, updateを持っていることが確認できます。

readable, writableの違い
readable, writableの違い

readableの値は$timeまたはwritableと同様にsubscribeで取得した値を利用してブラウザ上に表示させることはできますが読み込みしかできないため初期値から変わることはありません。それでは利用する用途がないように思われます。しかしwritableと同様に第二引数にset関数を引数に持つcallbackを設定することができます。外部から値を設定/更新することはできませんがこのset関数でみ値の更新を行うことができます。

set関数とsetIntervalを組み合わせて1秒ごとに更新される時刻を設定することができます。


<script>
  import { readable } from 'svelte/store';
  export let name;

  const time = readable(null, (set) => {
    set(new Date());

    const interval = setInterval(() => {
      set(new Date());
    }, 1000);

    return () => clearInterval(interval);
  });
</script>

<h2>{name}コンポーネント</h2>
<div>{$time}</div>
ブラウザ上に表示される時刻
ブラウザ上に表示される時刻

readableを利用する場合には第二引数が重要であることがわかります。

derived

derivedはこれまで説明したwritableとreadableを利用して設定を行います。つまりderivedは単独では利用することができず必ずwritableかreadableで作成した値が必要となります。

簡単な例で動作確認します。最初にwritableを利用してcountを定義します。次にdrivedを利用してdoubleCountを定義しますが引数にはwritableを利用して作成したcountを設定します。第二引数にはcallback関数を設定することができ、引数には第一引数で設定したwritableの値が入っています。writableの値を利用して2倍にします。


const count = writable(0);
const doubledCount = derived(count, ($count) => $count * 2);

readableと同様にdoubleCountはsubscribeのみしか持っているのでsubscribeするか$を利用してブラウザ上に表示させることができます。


<script>
  import { writable, derived } from 'svelte/store';
  export let name;

  const count = writable(0);
  const doubledCount = derived(count, ($count) => $count * 2);

  const increment = () => {
    count.update((value) => {
      return value + 1;
    });
  };
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{$count}</p>
  <p>ダブルカウント:{$doubledCount}</p>
</div>
<div>
  <button on:click={increment}>Up</button>
</div>

ボタンをクリックするとcountの値の2倍の値が表示されます。

ボタンをクリックすると2倍の値が表示
ボタンをクリックすると2倍の値が表示

callback関数には第一引数のwritableの値だけではなくset関数も利用することができるので数秒後にdoubleCountの値を更新するといったことも可能です。


const count = writable(0);
const doubledCount = derived(count, ($count, set) =>
  setTimeout(() => {
    set($count * 2);
  }, 1000)
);

設定するとボタンをクリックしてcountの値が1増えてから1秒後にdoubleCountの値が更新されます。

derivedの第一引数に設定できる値は1つだけではなく複数のwritableかreadableの値を指定することができます。複数指定する場合は配列で指定する必要があります。


<script>
  import { writable, derived } from 'svelte/store';
  export let name;

  const count = writable(0);
  const count2 = writable(5);
  const doubledCount = derived([count, count2], ([$count, $count2], set) =>
    setTimeout(() => {
      set($count * $count2);
    }, 1000)
  );

  const increment = () => {
    count.update((value) => {
      return value + 1;
    });
  };

  const increment2 = () => {
    count2.update((value) => {
      return value + 1;
    });
  };
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{$count}</p>
  <p>ダブルカウント:{$doubledCount}</p>
</div>
<div>
  <button on:click={increment}>Count Up</button>
  <button on:click={increment2}>Count2 Up</button>
</div>

ifによる条件分岐{#if…}

画面に表示させる内容を条件によって変更したい場合があります。その場合はHTMLのマークアップの部分でもif文を利用して条件分岐することができます。

countの数が5以上になるとincrementボタンを非表示にするためにif文による分岐を設定します。{#if 条件式}で分岐を行うことができます。ボタンをクリックして5以上になると表示されているincrementボタンが非表示となります。


<script>
  export let name = 'Counter';
  let count = 0;
  const increment = () => {
    count += 1;
  };
</script>

<h2>{name}コンポーネント</h2>
<div>
  <p>カウント:{count}</p>
</div>
{#if count < 5}
  <div>
    <button on:click={increment}>Up</button>
  </div>
{/if}

elseを設定したい場合は以下のように設定することができます。5以上になるとボタンの変わりに”これ以上数は増やせません。”の文字列が表示されます。


{#if count < 5}
  <div>
    <button on:click={increment}>Up</button>
  </div>
{:else}
  <p>これ以上数は増やせません。</p>
{/if}

if, elseだけではなくif elseも利用することができます。


{#if porridge.temperature > 100}
  <p>too hot!</p>
{:else if 80 > porridge.temperature}
  <p>too cold!</p>
{:else}
  <p>just right!</p>
{/if}

ライフサイクルフック

コンポーネントは突然表示され、突然非表示になるわけではなくブウラザ上に表示するためにコンポーネントの作成を開始してからブラウザから非表示され削除されるまでにさまざまな内部処理を行っています。その一連の流れをライフサイクルと呼びます。ライフサイクルの中のいくつかのポイントでHookという形で独自の処理を追加することができます。例えばonMount Hookを利用することでライフサイクルの中でコンポーネントがDOMにマウントされた直後に処理を追加することができます。またonDestory HookであればコンポーネントがDOMからアンマウントされてブラウザ上から消える前に処理を追加することができます。

onMount, onDestory以外にbeforeUpdate, afterUpdateの計4つのライフサイクルフックが提供されているのでどのタイミングでそれぞれのHookが実行されるのか実際のコードを利用して確認していきます。

Counterコンポーネントに4つのライフサイクルフックとscriptタグのトップレベルでconsole.logを実行しています。それぞれのconsole.logがどの順番でいつ表示させるのか確認していきます。


<script>
  import { beforeUpdate, onMount, onDestroy, afterUpdate } from 'svelte';

  beforeUpdate(() => {
    console.log('beforeUpdate execute');
  });
  onMount(() => {
    console.log('onMount execute');
  });
  afterUpdate(() => {
    console.log('afterUpdate execute');
  });
  onDestroy(() => {
    console.log('onDestroy execute');
  });
  console.log('script execute');
  let count = 0;
  const increment = () => {
    count += 1;
  };
</script>

<h2>Counterコンポーネント</h2>
<div>
  <p>カウント:{count}</p>
</div>
<div>
  <button on:click={increment}>Up</button>
</div>

CounterコンポーネントをimportするAppコンポーネントではshow変数を利用して表示、非表示を切り替えるように設定しています。


<script>
  import Counter from './Counter.svelte';
  let show = true;
</script>

<main>
  <h1>ライフサイクルフック</h1>
  <button on:click={() => (show = !show)}>Toggle</button>
  {#if show}
    <Counter />
  {/if}
</main>

<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  h1 {
    color: #ff3e00;
    text-transform: uppercase;
    font-size: 4em;
    font-weight: 100;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

設定後ブラウザでアクセスするとデベロッパーツールのコンソールには以下の順番でメッセージが表示されます。

  1. script execute
  2. beforeUpdate execute
  3. onMount execute
  4. afterUpdate execute

scriptタグのトップレベルのconsole.logが最初に実行されています。

画面表示直後のコンソール
画面表示直後のコンソール

Svelteのドキュメントを確認するとbeforeUpdateの最初の実行はonMountの前、afterUpdateはonMountの後に実行されると記述されているのでドキュメント通りの結果になっています。

“Up”ボタンをクリックしてカウント数をアップするとコンポーネントの更新が行われるのでbeforeUpdateとafterUpdateが実行されます。scriptタグのトップレベルのconsole.logが実行されることはありません。

カウントアップ後のメッセージ
カウントアップ後のメッセージ

最後にToggleボタンをクリックしてコンポーネントをアンマウントします。

アンマウントする際にonDestroyのみが実行されていることが確認できます。

コンポーネントのアンマウント
コンポーネントのアンマウント

実際に動作確認することで4つのライフサイクルフックがどのタイミングで実行されるのかを理解することができました。タイミングがわかればこれらのライフサイクルフックを利用することで独自の処理を追加することができます。

外部リソースからのデータ取得

外部リソースを行い取得したデータを表示する方法について確認を行っています。外部リソースには無料で利用できるJSONPlaceHolderを利用します。https://jsonplaceholder.typicode.com/usersにアクセスすると10名分のユーザ情報を取得することができます。

fetch関数によるユーザ情報の取得

UserList.svelteファイルを作成して、fetch関数を利用して情報を取得しますがfetch関数の実行はライフサイクルフックのonMountフックで実行します。


<script>
  import { onMount } from 'svelte';

  onMount(async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data = await response.json();
    console.log(data);
  });
</script>

<h2>ユーザ一覧</h2>

作成したUserList.svelteはAppコンポーネントからimportします。


<script>
  import UserList from './UserList.svelte';
</script>

<main>
  <h1>Hello</h1>
  <UserList />
</main>

<style>
//略
</style>

ブラウザで確認するとfetch関数によって取得したユーザ情報がデベロッパーツールのコンソールに表示されます。

取得したユーザ情報を確認
取得したユーザ情報を確認

{#each…ブロックによる配列の展開

fetch関数で取得したユーザ情報を変数usersに保存します。ユーザ情報を持つusersをHTMLのマークアップの部分で展開してブラウザ上に表示するために{#each…}を利用することができます。


<script>
  import { onMount } from 'svelte';
  let users = [];

  onMount(async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const data = await response.json();
    console.log(data);
    users = data;
  });
</script>

<h2>ユーザ一覧</h2>
<ul>
  {#each users as user}
    <li>{user.name}</li>
  {/each}
</ul>

ブラウザで確認すると取得したユーザ情報が表示されます。

fetchで取得したユーザ一覧を展開表示
fetchで取得したユーザ一覧を展開表示

{#each…を利用した場合に配列のindexを取得したい場合には下記のように記述することができます。


<ul>
  {#each users as user, index}
    <li>{index}.{user.name}</li>
  {/each}
</ul>

indexは0から始めるので以下のように表示されます。

#eachでindexを利用する方法
#eachでindexを利用する方法

ユーザリストの項目の一部を削除などによって変更した場合に期待通りに動作しない場合があります。その場合はリストを一意に識別するkeyを利用することで対応することができます。

取得したユーザ情報にはidという一意の識別子がついているのでidをkeyとして利用したい場合は以下のように記述することができます。


<ul>
  {#each users as user, index (user.id)}
    <li>{index}.{user.name}</li>
  {/each}
</ul>

#eachでは分割代入を利用することもできます。


<ul>
  {#each users as { name, id }, index (id)}
    <li>{index}.{name}</li>
  {/each}
</ul>

{#each…ブロックでは{:else}を利用することができます。利用することでデータが取得までに”Loaing…”をブラウザ上に表示させるといったことが可能になります。


<ul>
  {#each users as user}
    <li>{user.name}</li>
  {:else}
    <p>Loading...</p>
  {/each}
</ul>

{#await…ブロックを利用したfetch処理

HTMLのマークアップの部分で{#await…ブロックを利用することでpromise処理を行うことができます。ドキュメントを確認すると以下のように記述することができます。


{#await promise}
  <!-- promise is pending -->
  <p>waiting for the promise to resolve...</p>
{:then value}
  <!-- promise was fulfilled -->
  <p>The value is {value}</p>
{:catch error}
  <!-- promise was rejected -->
  <p>Something went wrong: {error.message}</p>
{/await}

#awaitのpromiseには関数を設定し、:thenのvalueには取得したデータを設定します。データ取得中は”Loading…”が画面上に表示されます。

ここではfetchUsers関数を定義してデータが取得できたらusersを戻します。ライフサイクルフックのonMountなどは利用しないので削除しています。


<script>
  const fetchUsers = async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const users = await response.json();
    if (response.ok) {
      return users;
    } else {
      throw new Error(response.status);
    }
  };
  let promise = fetchUsers();
</script>

<h2>ユーザ一覧</h2>

{#await promise}
  <p>Loading...</p>
{:then users}
  <ul>
    {#each users as user}
      <li>{user.name}</li>
    {/each}
  </ul>
{:catch error}
  <p>Something went wrong: {error.message}</p>
{/await}

正常に取得が完了した場合の画面は先ほどまでのユーザ一覧と変わりがありませんが存在しないURLを設定してエラーを発生させた場合には以下のように表示されます。

エラーが発生した場合の画面
エラーが発生した場合の画面

Styleの適用

Svelteではsvelteファイルのstyleタグの中でclassを定義してスタイリングを設定することができます。classを設定できるだけではなく変数と組み合わせことで条件によって適用するスタイリングを変更するといったことも可能です。classの適用方法は1つではないのでいくつか説明を行っています。

styleタグ内での設定

Appコンポーネントにh2タグを追加し、styleタグの中にh2のスタイリングを行います。


<script>
  import Counter from './Counter.svelte';
</script>

<main>
  <h1>Hello</h1>
  <h2>Styling</h2>
  <Counter />
</main>

<style>
//略
  h2 {
    color: #ff3e00;
    font-size: 2em;
    font-weight: 700;
  }
//略
</style>

styleタグの中で設定した内容が反映されていることがわかります。

h2タグにスタイリングを設定
h2タグにスタイリングを設定

AppコンポーネントにimportしているCounterコンポーネントにもh2タグが設定されていますがstyleタグの内容の適用範囲は設定したそのコンポーネント内のみとなるため他のコンポーネントへ適用されることはありません。

ブラウザでデベロッパーツールを利用してh2の要素を確認するとclassが追加されていることが確認できます。別のコンポーネントでh2タグにスタイリングすると別の名前のclass名が付与されます。

h2タグへのclassの設定
h2タグへのclassの設定

グローバルでの適用

もしグローバルに適用したい場合はApp.svelteファイルに:globalを利用して記述することでCounterコンポーネントのh2タグにもスタイルが適用されます。


:global(h2) {
	color: #ff3e00;
	font-size: 2em;
	font-weight: 700;
}

App.svelteではなくCounter.svelteに記述してもApp, Counterコンポーネントのh2タグの両方にスタイルが設定されます。

この場合は先ほどのようにh2タグにclassが追加されることはなくh2にスタイルが設定されるためグローバルにスタイルが設定されます。

グローバルのスタイルの適用
グローバルのスタイルの適用

変数によるclassの設定

変数emphasisを定義して値をactiveとします。activeはstyleタグの中で定義されているclassです。変数を利用してclassを適用したい場合にはカーリブレースを利用して変数名を設定します。この場合はactiveクラスが適用されます。


<script>
  let emphasis = 'active';
</script>

<main>
  <h1>Hello</h1>
  <h2 class={emphasis}>Styling</h2>
</main>

<style>
  .active {
    font-size: 3em;
    font-weight: 700;
  }

//略
</style>

もしclassを””(ダブルクオテーション)で設定した場合にはemphasisクラスが適用されます(emphasisクラスが定義されている場合)。


<h2 class="emphasis">Styling</h2>

条件によるclassの設定(三項演算子)

JavaScriptの三項演算子と変数を利用して変数の値によって適用するclassを変えることができます。

変数emphasisの値がtrueかfalseかによって適用されるクラスが動的に変わります。下記ではtrueの場合はactiveクラスが適用され、falseの場合は何も適用されません。


<script>
  let emphasis = true;
</script>

<main>
  <h1>Hello</h1>
  <h2 class={emphasis ? 'active' : ''}>Styling</h2>
</main>

<style>
  .active {
    font-size: 3em;
    font-weight: 700;
  }

//略
</style>

複数のclassを適用したい場合には下記のように記述することができます。


<script>
  let emphasis = true;
</script>

<main>
  <h1>Hello</h1>
  <h2 class={emphasis ? 'active underline' : ''}>Styling</h2>
</main>

<style>
  .active {
    font-size: 3em;
    font-weight: 700;
  }
  .underline {
    text-decoration: underline;
  }
//略
</style>

underlineクラスはemphasisの値によらず適用したい場合には下記のように記述することができます。


<h2 class="underline {emphasis ? 'active' : ''}">Styling</h2>

条件によるclassの設定

条件によるclassの設定は下記の方法でも行うことができます。emphasisがtrueの場合のみactiveクラスが適用されます。falseの場合は何も適用されません。


<script>
  let emphasis = true;
</script>

<main>
  <h1>Hello</h1>
  <h2 class:active={emphasis}>Styling</h2>
</main>

<style>
  .active {
    font-size: 3em;
    font-weight: 700;
  }
//略
</style>

カーリーブレース{}の中にはboolean値を持つ変数だけではなく条件文を設定することもできます。


<script>
  let emphasis = 'emphasis';
</script>

<main>
  <h1>Hello</h1>
  <h2 class:active={emphasis === 'emphasis'}>Styling</h2>
</main>
//略

Formの設定

input, checkboxなどのSvelteにおける設定方法について説明を行っています。

inputの設定(bindディレクティブ)

scriptタグの中に変数を定義して{}で変数を囲むことでブラウザ上に表示することができます。


<script>
  let name = 'John';
</script>

<main>
  <h1>Hello {name}</h1>
</main>

<style>
//略
</style>

input要素のvalue属性にnameの値を設定することでinput要素に変数nameで設定した値を表示させることができます。


<script>
  let name = 'John';
</script>

<main>
  <h1>Hello {name}</h1>
  <input type="text" value={name} />
</main>

<style>
//略
</style>

input要素に”John”が表示されましたがinput要素の文字列を変更しても画面に表示されているnameの値には影響はありません。

One way Binding
One way Binding

input要素に入力した値を変数nameに反映させるためにbindディレクティブを利用することで実現することができます。


<script>
  let name = 'John';
</script>

<main>
  <h1>Hello {name}</h1>
  <input type="text" bind:value={name} />
</main>

<style>
//略
</style>

input要素の文字列を変更するとブラウザ上に表示されている文字列も一緒に変更されることが確認できます。scriptタグで定義した変数とinput要素のフィールドに表示されている文字列が必ず同期していることを意味します。

Two way Binding
Two way Binding

inputの設定(inputイベント)

bindディレクティブを利用することでscriptタグで定義した変数とinput要素のフィールドに表示されている文字列を同期することができましたがbindディレクティブを利用しない場合でもon:inputイベントを利用して同様の設定を行うことができます。


<script>
  let name = 'John';
  const handleInput = (e) => {
    name = e.target.value;
  };
</script>

<main>
  <h1>Hello {name}</h1>
  <input type="text" on:input={handleInput} value={name} />
</main>

<style>
//略
</style>

文字を入力する度にon:inputイベントが発火されhandleInput関数が実行されます。handleInput関数ではeventオブジェクトを利用してinput要素に設定されている値をe.target.valueで取得してnameに設定しています。

bind:thisによるDOMの参照

例えばページを開いた時にinput要素にフォーカスを当てたい場合など直接にDOMにアクセスを行いたい場合があります。その場合はDOMへの参照のためbind:thisを利用することができます。

DOM要素の情報を保存する変数inputElを定義します。参照したいinput要素にbind:this={inputEL}を設定します。ライフサイクルフックのonMount後にはコンポーネントの要素にアクセス可能なのでinputEl.focus()でフォーカスを設定します。ページを開くとinput要素にフォーカスされた状態で表示されます。


<script>
  import { onMount } from 'svelte';
  let name = 'John';
  let inputEl;
  onMount(() => {
    inputEl.focus();
    console.log(inputEl);
  });
</script>

<main>
  <h1>Hello {name}</h1>
  <input type="text" bind:value={name} bind:this={inputEl} />
</main>

<style>
//略
</style>

inputElの中身はconsole.logを設定していたのでブラウザのコンソールで確認すると<input type=”text”>が表示されます。input要素にアクセスできていることがわかります。そのためinput要素の値を取得したい場合にはinputEl.valueで行うことができます。

scriptタグのトップレベルでinputEl.focus()を実行するとCannot read properties of undefined(reading ‘focus’)エラーが表示されます。

checkboxの設定

bindディレクティブを利用することでcheckboxの値を取得することができます。forget_password変数を定義してデフォルト値をfalseとします。inputのtype属性にcheckboxを設定してbind:checked={forget_password}を設定します。ブラウザ上にpassword_forgetの値を表示させているのでチェックボックスにチェックを入れるとforget_passwordの値はfalseからtrueに変わることが確認できます。


<script>
  let name = 'John';
  let password_forget = false;
</script>

<main>
  <h1>Hello {name} {password_forget}</h1>
  <div>
    <input type="text" bind:value={name} />
  </div>
  <div>
    <input type="checkbox" bind:checked={password_forget} /> forget password
  </div>
</main>

<style>
//略
</style>
bindディレクティブによるcheckboxの設定
bindディレクティブによるcheckboxの設定

formタグの設定

一般的はinput要素などで構成された入力フォームは入力が完了するとボタンをクリックしてサーバに入力内容の送信を行います。ここではサーバへの送信は行いませんがボタンをクリックした後に入力した内容を表示させます。

formタグにはon:submitイベントを設定することができ、”送信”ボタンをクリックするとhandleSubmit関数が実行されます。


<script>
  let name = 'John';
  const handleSubmit = () => {
    console.log(name);
  };
</script>

<main>
  <h1>Hello {name}</h1>
  <form on:submit={handleSubmit}>
    <div>
      <input type="text" bind:value={name} />
    </div>
    <button type="submit">送信</button>
  </form>
</main>

<style>
//略
</style>

実際にブラウザで”送信”ボタンをクリックするとブラウザのコンソールには一瞬name変数に保存された文字列が表示されますがすぐに消えます。理由はブラウザのリロードが発生しているためです。formのデフォルトの動作でsubmitが行われるとページのリロードが行われます。デフォルトの動作を停止させるためにEvent Modifiers(イベント修飾子)が用意されています。

イベントの横にpreventDefaultを設定することでページのリロードが行われなくなりコンソールにはinput要素に入力した文字列が表示されます。


<form on:submit|preventDefault={handleSubmit}>

イベント修飾子のpreventDefaultを利用せずeventオブジェクトを利用することで同様の設定を行うことができます。


const handleSubmit = (e) => {
  e.preventDefault();
  console.log(name);
};

その他には以下の修飾子が準備されています。

Event Modifiersの一覧
Event Modifiersの一覧

Transitionの設定

DOMから要素が表示されたり消えたりする場合にCSSや他のライブラリを追加することなくSvelteが持つtransitionディレクティブを利用してアニメーションを設定することがができます。

fadeの設定

svelte/transitionからfade関数をimportしてtransitionを利用したい要素に対してtransition:fadeを設定します。これだけで設定は完了です。


<script>
  import { fade } from 'svelte/transition';
  let show = true;
</script>

<main>
  <h1>Transition</h1>
  <button on:click={() => (show = !show)}>Toggle</button>
  {#if show}
    <p transition:fade>Fadeの動作確認</p>
  {/if}
</main>

<style>
//略
</style>

Toggleボタンをクリックするとゆっくりと”Fadeの動作確認”の文字列が消えて、再度Toggleボタンをクリックするとゆっくりと”Fadeの動作確認”の文字列が表示されます。もしtransitionを設定していない場合は即座に表示・非表示が切り替わります。

toggleボタンをクリックしてtransitionの動作確認
toggleボタンをクリックしてtransitionの動作確認

最初のレンダリングでは”Fadeの動作確認”はfadeが行われず表示されます。最初のレンダリングでもtransitionを適用させたい場合にはmain.jsのAppコンポーネントの引数にintroプロパティでtrueを設定します。introプロパティの値はデフォルトではfalseに設定されています。これで最初のレンダリングにもゆっくりと文字列が表示されます。


import App from './App.svelte';

const app = new App({
  target: document.body,
  intro: true,
});

export default app;

先ほど確認したpタグのようなDOM要素ではなくimportしたコンポーネントを利用して実行すると”Transitions can only be applied to DOM elements, not components”のエラーメッセージが表示されます。コンポーネントではtransitionディレクティブを利用することはできません。


<script>
  import Counter from './Counter.svelte';
  import { fade } from 'svelte/transition';
  let show = true;
</script>

<main>
  <h1>Transition</h1>
  <button on:click={() => (show = !show)}>Toggle</button>
  {#if show}
    <Counter transition:fade />
  {/if}
</main>

<style>
//略
</style>

パラメータの設定

fadeにはパラメータを設定することができ、パラメータを設定することでfadeのデフォルトの動作を変えることができます。fadeのパラメータはdelay, duration, easingを設定することができます。

delayはアニメーションが開始されるまでの時間、durationはアニメーションの時間、easingはアニメーションの速度の変化を設定することができます。

easingを初めて聞いた人はわかりにくいかもしれませんが例えばscaleなどを利用して文字の大きさを段々と大きくする場合があります。デフォルトのeasingでは同じページでサイズが変化していますが別のeasingを設定することで最初は急激に大きくなり、残りの時間ではゆっくりとサイズが大きくなるといったようなアニメーションに速度を持たせることができます。

パラメータを以下のように設定することができ、durationを3000(3秒)に設定しているのでゆっくり表示・非表示が切り替わります。


{#if show}
  <p transition:fade={{ delay: 300, duration: 3000 }}>Fadeの動作確認</p>
{/if}

fade以外にもblur, fly, slide, scale, draw, crossfadeなどの7つの関数があります。関数によって設定できるパラメータが異なるのでドキュメントを確認しながら設定することになります。

flyの設定

fade以外の関数も動作確認しておきましょう。

flyはパラメータの位置の設定も行えるため要素が移動するアニメーションを設定することができます。パラメータのx, yで位置の設定を行うことができ、下記のようにx:100を設定することで表示から非表示になる際にx方向に移動しながら非表示になっていきます。


<script>
  import { fly } from 'svelte/transition';
  let show = true;
</script>

<main>
  <h1>Transition {show}</h1>
  <button on:click={() => (show = !show)}>Toggle</button>
  {#if show}
    <p transition:fly={{ x: 100 }}>Fadeの動作確認</p>
  {/if}
</main>

in, outの設定

表示する場合と非表示になる場合のアニメーションを別に設定することもできます。その場合はtransitionではなくin, outを利用できます。下記ではin, outを利用して非表示になる場合にはfadeが適用され、表示になる場合にはflyが適用されます。


<script>
  import { fly, fade } from 'svelte/transition';
  let show = true;
</script>

<main>
  <h1>Transition {show}</h1>
  <button on:click={() => (show = !show)}>Toggle</button>
  {#if show}
    <p in:fly={{ x: 100 }} out:fade>Fadeの動作確認</p>
  {/if}
</main>

{#kye..}ブロッックの利用

アニメーションは表示・非表示の切り替え時のみだけではなく値が更新される場合にも適用することができます。Counterコンポーネントのcountの値はボタンをクリックすると値が増えていくのでKeyブロックを設定{#key count}…{/key}します。keyブロックを設定することでcountの値が更新されるとkeyブロックの間にある要素が再作成されるようになります。そこにtransitionを利用してアニメーションを設定します。

in:flyを設定することでcountの数が増える度にアニメーションが実行されます。


<script>
  import { fly } from 'svelte/transition';
  export let name = 'Counter';
  let count = 0;
  const increment = () => {
    count += 1;
  };
</script>

<h2>{name}コンポーネント</h2>

<p>
  カウント:
  {#key count}
    <span style="display:inline-block" in:fly={{ y: -20 }}>{count}</span>
  {/key}
</p>

<div>
  <button on:click={increment}>Up</button>
</div>

CounterコンポーネントはAppコンポーネントでimportしています。


<script>
  import Counter from './Counter.svelte';
</script>

<main>
  <h1>Hello!</h1>
  <Counter />
</main>

<style>
//略
</style>

イベントの設定

transitionの開始、終了時に何か処理を実行したい場合にはイベントを利用することができます。4つのイベントが準備されており、それぞれのタイミングで処理を追加することができます。

  • introstart
  • introend
  • outrostart
  • outroend

<script>
  import { fade } from 'svelte/transition';
  let show = true;
</script>

<main>
  <h1>Transition</h1>
  <button on:click={() => (show = !show)}>Toggle</button>
  {#if show}
    <p
      transition:fade
      on:introstart={() => console.log('intro started')}
      on:outrostart={() => console.log('outro started')}
      on:introend={() => console.log('intro ended')}
      on:outroend={() => console.log('outro ended')}
    >
      Fadeの動作確認
    </p>
  {/if}
</main>

<style>
//略
</style>

Toglleボタンをクリックすると非表示になるためコンソールには’outro started’, ‘outro ended’が順番に表示されます。再度Toggleボタンをクリックすると非表示から表示になるため’intro started’, ‘intro ended’が順番に表示されます。

コンポーネントの基礎

コンポーネント毎にファイルをわけimportすることで再利用できること、propsを利用してコンポーネントから子コンポーネントにデータが渡せることは確認しました。それ以外にもコンポーネントでさまざまな機能を利用することができるのでコンポーネント間の通信、Slotを中心に説明を行なっています。

コンポーネント間の通信

コンポーネント内で定義した変数や関数を他のコンポーネントで直接利用することができません。そのためコンポーネント間でデータを共有するための方法がいつくか用意されています。

1つの方法はすでに確認済みであるコンポーネントから子コンポーネントにデータを渡したい場合にはpropsを利用することができます。これまでは片方向での通信でした。

その逆の子コンポーネントから親コンポーネントにデータを渡したり通信したい場合にはpropsとは異なるイベント機能を利用することができます。イベントを利用することで両方向の通信が可能になります。

動作確認確認のためChild.svelteファイルを作成します。Alertボタンを設定します。


<script>
</script>

<h2>Childコンポーネント</h2>
<button>Alert</button>

Appコンポーネントで作成したChildコンポーネントをimportしてます。


<script>
  import Child from './Child.svelte';
</script>

<main>
  <h1>Component</h1>
  <Child />
</main>

<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
    margin: 0 auto;
  }

  h1 {
    color: #ff3e00;
    text-transform: uppercase;
    font-size: 4em;
    font-weight: 100;
  }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

ブラウザには以下の画面が表示されます。

Childコンポーネントの表示
Childコンポーネントの表示

子コンポーネントの”Alert”ボタンをクリックしても現在の状態では親コンポーネント側では”Alert”ボタンがクリックされたことはわかりません。

子コンポーネントで”Alert”ボタンが押されたことを親コンポーネントに伝えるためcreatenDispatcherを利用します。

button要素にclickイベントを設定しボタンをクリックするとclicked関数が実行されます。clicked関数の中ではcreateEventDispatcher関数から作成したdispatch関数を利用してalertButtonClickedイベントを発火します。イベント名前は任意の名前をつけることができます。


<script>
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();
  const clicked = () => {
    dispatch('alertButtonClicked');
  };
</script>

<h2>Childコンポーネント</h2>
<button on:click={clicked}>Alert</button>

Childからdispatch関数を利用して発火されたイベントは発火されただけでは何も起こりません。イベントは発火だけではなく受け取る処理が必要になります。親コンポーネントであるAppコンポーネントで受け取るため以下の設定を行います。on:の後にはChildコンポーネントで設定したイベント名を設定します。イベントを受け取るとdisplayAlert関数を実行します。


<Child on:alertButtonClicked={displayAlert} />

displayAlert関数ではalert関数を実行します。


<script>
  import Child from './Child.svelte';
  const displayAlert = () => {
    alert('子コンポーネントからイベントを受け取りました。');
  };
</script>

<main>
  <h1>Component</h1>
  <Child on:alertButtonClicked={displayAlert} />
</main>

これで設定は完了です。ブラウザにある”Alert”ボタンをクリックしてください。ブラウザ上にalertが表示されます。

子コンポーネントからのイベントを取得しalertを実行
子コンポーネントからのイベントを取得しalertを実行

子コンポーネントから親コンポーネントに通信を行う方法を確認することができました。次はイベントと一緒にデータを渡す方法を確認します。

データはdispatch関数の第二引数に設定することができます。error変数を定義して文字列を設定し、dispatch関数の第二引数に設定します。


<script>
  import { createEventDispatcher } from 'svelte';

  let error = 'エラーが発生しています。';

  const dispatch = createEventDispatcher();
  const clicked = () => {
    dispatch('alertButtonClicked', error);
  };
</script>

<h2>Childコンポーネント</h2>
<button on:click={clicked}>Alert</button>

dispatch関数から渡されたデータはeventオブジェクトを利用して取得することができます。eventオブジェクトのdetailプロパティにデータが入っています。


<script>
  import Child from './Child.svelte';
  const displayAlert = (e) => {
    alert(e.detail);
  };
</script>

<main>
  <h1>Component</h1>
  <Child on:alertButtonClicked={displayAlert} />
</main>

<style>
//略
</style>

イベントから渡されたデータを表示することができました。propsのように設定がシンプルではありませんが慣れればそれほど難しいものではありません。

イベントから受け取ったデータを表示
イベントから受け取ったデータを表示

propsやイベントを利用したコンポーネント間の通信の変わりにSvelteではContext APIやwritable storeを利用したデータの共有方法が提供されています。これらの機能は後ほど説明します。

Slotの設定

親コンポーネントから子コンポーネントに”コンテンツ”を渡したい場合にSlotを利用することができます。動作確認を行うためにUser.svelteファイルを作成して以下を記述します。

<slot />の場所に親コンポーネントから渡されるコンテンツが表示されます。


<script>
</script>

<p>この人の名前は<slot />です。</p>

子コンポーネント側に渡したいコンポーネントはコンポーネントタグの間に挿入します。


<script>
  import User from './User.svelte';
</script>

<main>
  <h1>Slot</h1>
  <User>John Doe</User>
</main>

<style>
//略
</style>

ブラウザで確認すると下記のように表示されます。

Slotの内容を表示
Slotの内容を表示

ここまでの動作確認であれば”John Doe”をpropsで渡せばいいのでは思うかと思います。Slotを利用する例としてはレイアウトファイルなどを想像すると簡単かと思います。レイアウトファイルではヘッダーやサイドバー、フッターなどの情報はすべてのページで共通です。異なるは各ページに表示されるコンテンツでSlotを利用することでレイアウトファイルのコンテンツ部分のみSlotを通してコンテンツを渡すといったことが行えます。

Slotで渡すコンテンツは通常は文字列ではなくHTMLのマークアップになるのでSlotを利用してHTMLマークアップが渡せることを確認します。


<script>
</script>

<h2>プロファイル</h2>
<slot />

pタグを利用したHTML分をUserタグの間に挿入します。


<script>
  import User from './User.svelte';
</script>

<main>
  <h1>Slot</h1>
  <User>
    <p>私の名前はJohn Doeです。</p>
    <p>メールアドレスはjohn@exmaple.com</p>
  </User>
</main>

文字列だけではなくHTMLも渡されることが確認できました。

Slotを利用してHTML文を渡した場合
Slotを利用してHTML文を渡した場合

slotのデフォルト値を設定することができ、もし親コンポーネントからコンテンツが渡されない場合はデフォルトで設定したコンテンツが表示されます。


<script>
</script>

<h2>プロファイル</h2>
<slot><p>何もコンテンツが渡されません</p></slot>

Userタグの間には何もコンテンツを設定しません。


<main>
  <h1>Slot</h1>
  <User />
</main>

ブラウザで確認すると”何もコンテンツが渡されません”が表示されます。

名前付きSlot(named Slot)

1つのコンポーネントに複数のslotを設定することができます。複数slotが存在する場合はそれぞれのSlotを識別するために名前をつける必要があります。name属性を設定して識別できる名前を設定します。name属性が設定されていないものはデフォルトのSlotとなります。


<script>
</script>

<h2>プロファイル</h2>
<ul>
  <li>名前:<slot>名無しの権兵衛</slot></li>
  <li>年齢:<slot name="age">記入なし</slot></li>
  <li>
    メールアドレス:<slot name="email">メールアドレスを持っていません。</slot>
  </li>
</ul>

Slotにはデフォルト値も設定されているので何も設定しない場合と各Slotに対応するコンテンツを設定した場合の2つのUserコンポーネントを設定します。タグにslot属性をつけてUserコンポーネントでつけた名前を設定します。slot属性がついていないものがデフォルトとなります。


<script>
  import User from './User.svelte';
</script>

<main>
  <h1>Slot</h1>
  <User
    >John Doe
    <span slot="age">30</span>
    <span slot="email">john@exmaple.com</span>
  </User>
  <User />
</main>

ブウラザで確認するとslotを設定しない場合にもデフォルト値が表示されていることがわかります。

名前付きSlotを利用した場合
名前付きSlotを利用した場合

svelte:fragment

親コンポーネントから名前付きSlotでコンテンツを渡す時slot属性を設定するためspan要素を使っています。もし要素を利用したくない場合にはsvelte:fragmentを利用することができます。


<User
  >John Doe
  <svelte:fragment slot="age">30</svelte:fragment>
  <span slot="email">john@exmaple.com</span>
</User>

ブラウザのデベロッパーツールで要素を確認することでsvelte:fragmentを利用した場合とspan要素を利用した場合の違いを確認することができます。年齢には要素がありませんがメールアドレスにはspan要素が確認できます。

svelte.fragmentを利用した場合
svelte.fragmentを利用した場合

$$slots

$$slotsは親コンポーネントからslotが渡されているかどうか確認することができるオブジェクトです。$$slotsにどのような情報が入っているか確認するためにscriptタグでconsole.log($$slots)を実行します。


<script>
  console.log($$slots);
</script>

<h2>プロファイル</h2>
<ul>
  <li>名前:<slot>名無しの権兵衛</slot></li>
  <li>年齢:<slot name="age">記入なし</slot></li>
  <li>
    メールアドレス:<slot name="email">メールアドレスを持っていません。</slot>
  </li>
</ul>

ブラウザのデベロッパーツールのコンソールを確認するとすべてのslotを設定している場合はslotの名前とtrueがペアで表示されます。


{email: true, age: true, default: true}

例えばemailのSlotを設定していない場合にはfalseになるわけではなくemailのプロパティが表示されません。


{age: true, default: true}

slotでコンテンツが渡されない場合にslotのデフォルト値を表示するのではなく条件分岐を利用してその項目自体を表示させないといったことが可能です。

slotでageが渡されない場合にはli要素自体を表示させません。


<script>
  console.log($$slots);
</script>

<h2>プロファイル</h2>
<ul>
  <li>名前:<slot>名無しの権兵衛</slot></li>
  {#if $$slots.age}
    <li>年齢:<slot name="age">記入なし</slot></li>
  {/if}
  <li>
    メールアドレス:<slot name="email">メールアドレスを持っていません。</slot>
  </li>
</ul>

ageのslotをコメントします。


<main>
  <h1>Slot</h1>
  <User
    >John Doe
    <!-- <svelte:fragment slot="age">30</svelte:fragment> -->
    <span slot="email">john@exmaple.com</span>
  </User>
</main>

年齢の項目は表示されていないことが確認できます。

$$slotsを利用した例
$$slotsを利用した例

<slot key={value}>の設定

slotのpropsを利用することで子コンポーネントのデータを親コンポーネントに渡すことができます。

ここまでの設定ではUserコンポーネントのslotタグは親から渡されるコンテンツを表示するために利用してきましたがslotタグにはpropsを設定することができます。下記では定義した変数userをslotのpropsに設定しています。<slot {user}>は<slot user={user}>と同じ意味です。


<script>
  let user = {
    firstName: 'John',
    lastName: 'Doe',
    age: '25',
    sex: '男性',
  };
</script>

<h2>プロファイル</h2>
<slot {user}>{user.lastName}</slot>

親側のコンポーネントは子コンポーネントのslotのpropsから渡されるデータをletを利用して取得することができます。子コンポーネントから渡さらたuserを利用して子コンポーネントに渡すコンテンツとして利用しています。let:userはlet:user={user}と同じ意味です。


<script>
  import User from './User.svelte';
</script>

<main>
  <h1>Slot</h1>
  <User let:user><h2>{user.firstName}</h2></User>
</main>

ブラウザ上に表示されるのはUserコンポーネントで定義したuserオブジェクトのfirstNameになります。

slotのpropsで戻された値が表示
slotのpropsで戻された値が表示

先ほどの例ではUserコンポーネントで定義したuserを親コンポーネントに戻していましたが親コンポーネントで定義したuserをpropsでUserコンポーネントに渡しslotのpropsで親コンポーネントに戻すこともできます。デフォルトではfirstNameとlastNameを表示させるようになっているがUserコンポーネントを変更することなくfistNameのみ表示させるように変更するといったことが可能になります。

slotで戻すpropsの名前は任意の名前をつけることができるのでthingにしています。


<script>
  export let user;
</script>

<h2>プロファイル</h2>
<slot thing={user}>{user.firstName} {user.lastName}</slot>

propsはthingという名前渡されるのでlet:の後はthingを設定します。


<script>
  import User from './User.svelte';
  let user = {
    firstName: 'Jane',
    lastName: 'Doe',
    age: '25',
    sex: '男性',
  };
</script>

<main>
  <h1>Slot</h1>
  <User {user} let:thing><h2>{thing.firstName}</h2></User>
  <User {user} />
</main>

<style>
//略
</style>

戻されたuser情報を利用して表示したUserコンポーネントとデフォルトのまま表示させたUserコンポーネントを表示しています。

slotのpropsを利用した場合としていない場合
slotのpropsを利用した場合としていない場合

複数のユーザオブジェクトを含む配列の場合にもslot propsを利用することができます。{users}はusers={users}と同じ意味です。Userコンポーネントにpropsでusersを渡し、userが戻されるので戻されたuserを使ってslotのコンテンツとして子コンポーネントに渡しています。


<script>
  import User from './User.svelte';
  const users = [
    {
      id: 1,
      firstName: 'John',
      lastName: 'Doe',
      email: 'john@example.com',
    },
    {
      id: 2,
      firstName: 'Kevin',
      lastName: 'Wood',
      email: 'kevin@example.com',
    },
    {
      id: 3,
      firstName: 'Harry',
      lastName: 'Bosch',
      email: 'harry@test.com',
    },
  ];
</script>

<main>
  <h1>Slot</h1>
  <User {users} let:user>{user.email}</User>
</main>

<style>
//略
</style>

propsで受け取ったusersを{#each}で展開して、slotのpropsでuserを親コンポーネントに渡しています。親コンポーネントからslotが渡されない場合には名前がフルネームで表示されます。


<script>
  export let users;
</script>

<ul>
  {#each users as user}
    <li><slot {user}>{user.firstName} {user.lastName}</slot></li>
  {/each}
</ul>

名前付きSlotを利用している場合には下記のように記述することができます。


<script>
  import User from './User.svelte';
  const users = [
    {
      id: 1,
      firstName: 'John',
      lastName: 'Doe',
      email: 'john@example.com',
    },
    {
      id: 2,
      firstName: 'Kevin',
      lastName: 'Wood',
      email: 'kevin@example.com',
    },
    {
      id: 3,
      firstName: 'Harry',
      lastName: 'Bosch',
      email: 'harry@test.com',
    },
  ];
</script>

<main>
  <h1>Slot</h1>
  <User {users}>
    <h2 slot="title">ユーザ一覧</h2>
    <span slot="name" let:user>{user.email}</span>
  </User>
</main>
//略

<script>
  export let users;
</script>

<slot name="title"><p>ユーザ</p></slot>
<ul>
  {#each users as user}
    <li><slot name="name" {user}>{user.firstName} {user.lastName}</slot></li>
  {/each}
</ul>

Context API

親コンポーネントから子コンポーネントにデータを渡したい場合にはpropsが利用できます。コンポーネント間の階層があまりない場合には特に問題はありませんが階層が深くなればなるほどデータを届けるまでに経由するコンポーネントが増えるため管理が大変になりました。

例えばApp, ComponentA, ComponentB, ComponentCの4つのコンポーネントが階層になっている場合AppからComponentCで利用するデータを渡すためにはComponentA, ComponentBでもpropsの設定を行う必要があります(props drilling)。propsではなくContext APIを利用すればAppからComponentCにデータを渡すのにComponentB, ComponentCを経由せずに直接渡すことができます。

propsの場合

propsでAppコンポーネントからComponentCにデータを渡すためComponentA, ComponentB, ComponentCを作成します。


<script>
  import ComponentA from './ComponentA.svelte';
  const user = {
    name: 'John Doe',
    email: 'john@example.com',
  };
</script>

<main>
  <h1>Context API</h1>
  <ComponentA {user} />
</main>

<style>
//略
</style>

<script>
  import ComponentB from './ComponentB.svelte';
  export let user;
</script>

<h2>ComponentA</h2>
<ComponentB {user} />

<script>
  import ComponentC from './ComponentC.svelte';
  export let user;
</script>

<h2>ComponentB</h2>
<ComponentC {user} />

<script>
  export let user;
</script>

<h2>ComponentC</h2>
<h3>ユーザ</h3>
<div>name:{user.name}</div>
propsを利用してuserオブジェクトを表示
propsを利用してuserオブジェクトを表示

Context APIを利用した場合

propsからContext APIを利用した方法に変更します。

Context APIを利用する場合はsvelteからsetContext関数をimportしてsetContext関数の第一引数のContextのkeyに’user’、第二引数のCotextの値に渡したいデータをを設定します。ここでは渡すデータにオブジェクトを設定しています。keyに設定した値は他のコンポーネントでデータを取り出す際に利用します。各コンポーネントで設定したpropsの設定は必要ないので削除します。


<script>
  import ComponentA from './ComponentA.svelte';
  import { setContext } from 'svelte';

  setContext('user', {
    name: 'John Doe',
    email: 'john@example.com',
  });
</script>

<main>
  <h1>Context API</h1>
  <ComponentA />
</main>

<style>
//略
</style>

<script>
  import ComponentB from './ComponentB.svelte';
</script>

<h2>ComponentA</h2>
<ComponentB />

<script>
  import ComponentC from './ComponentC.svelte';
</script>

<h2>ComponentB</h2>
<ComponentC />

ComponentCでuserオブジェクトのnameを表示するためAppコンポーネントで設定したkeyの’user’をgetContext関数の引数に指定してデータを取り出します。


<script>
  import { getContext } from 'svelte';

  const user = getContext('user');
</script>

<h2>ComponentC</h2>
<h3>ユーザ</h3>
<div>name:{user.name}</div>

propsと同様にユーザ名が表示されます。このようにContext APIを利用することでコンポーネント間の階層が深い場合には簡単にデータを渡すことができます。

データの更新方法(storeの利用)

setContextで設定した値を更新するためにボタンを追加します。追加したボタンにはclickイベントを設定しボタンをクリックするとchangeName関数が実行されます。changeName関数ではuserオブジェクトの更新を行います。


<script>
  import ComponentA from './ComponentA.svelte';
  import { setContext } from 'svelte';

  const user = {
    name: 'John Doe',
    email: 'john@example.com',
  };

  setContext('user', user);

  const changeName = () => {
    user.name = 'Jane Doe';
  };
</script>

<main>
  <h1>Context API</h1>
  <button on:click={changeName}>change Name</button>
  <ComponentA />
</main>

<style>
//略
</style>

ボタンをクリックしてもブラウザ上の名前が更新されることもなくエラーも発生しません。

この方法ではContext APIで設定したデータの値は更新できません。Context APIで設定したデータを更新するためにはstoreのwritableを利用する必要があります。writableのstoreについてはこの後の章で説明を行っています。そちらを確認してから再度戻ってきたようが理解するのは簡単かもしれません。

svelte/storeからimportしたwritableの引数にuserオブジェクトを設定します。writable関数を実行した結果を保存したuserをsetContextでContextの値に設定します。


<script>
  import ComponentA from './ComponentA.svelte';
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  const user = writable({
    name: 'John Doe',
    email: 'john@example.com',
  });

  setContext('user', user);

  const changeName = () => {
    user.name = 'Jane Doe';
  };
</script>

writableを追加したためブラウザ上に表示されていたユーザ名はundefinedとなります。undefinedではなく正しいユーザ名を表示するためにComponentCコンポーネントを更新します。userの前に$userを設定します。writableを利用した場合に更新したデータを受け取るためにはsubscribeしなければなりません。$を利用することでsubscribeに関するコードを省略しデータにアクセスすることができます。


<script>
  import { getContext } from 'svelte';

  const user = getContext('user');
</script>

<h2>ComponentC</h2>
<h3>ユーザ</h3>
<div>name:{$user.name}</div>

userから$userに変更することでuserオブジェクトの更新が行われると更新が反映されます。

changeName関数でも同様に$userを利用してnameプロパティの値を更新します。


<script>
  import ComponentA from './ComponentA.svelte';
  import { setContext } from 'svelte';
  import { writable } from 'svelte/store';

  const user = writable({
    name: 'John Doe',
    email: 'john@example.com',
  });

  setContext('user', user);

  const changeName = () => {
    $user.name = 'Jane Doe';
  };
</script>

<main>
  <h1>Context API</h1>
  <button on:click={changeName}>change Name</button>
  <ComponentA />
</main>

設定は完了したので”change Name”ボタンをクリックするとユーザ名は”John Doe”から”Jane Doe”に更新されます。

$を利用しない場合のコードも確認しておきます。先ほど説明した通り更新した内容を受け取るためにはsubscribeを行う必要があります。またsubscribeした場合は必ずunsubscribeを行う必要があります。unsubscribeを行わない場合はコンポーネントがアンマウントされた場合などsubscribeの処理が残された状態になりメモリリークにつながる可能性もあります。


<script>
  import { getContext, onDestroy } from 'svelte';

  let user;
  const userContext = getContext('user');

  const unscribe = userContext.subscribe((value) => {
    user = value;
  });
  onDestroy(unscribe);
</script>

<h2>ComponentC</h2>
<h3>ユーザ</h3>
<div>name:{user.name}</div>

changeName関数の更新処理で$を利用しない場合はupdateを利用します。


const changeName = () => {
  user.update((user) => {
    return { ...user, name: 'Jane Doe' };
  });
};

Store

svelte/storeモジュールについてはwritable, readable, derived, getがあることと機能の説明を行いましたがここではオブジェクトを利用して複数コンポーネントから利用する場合のデータ共有に注目して動作確認を行っています。先ほどのsvelte/storeモジュールと説明が重複している箇所があります。

SvelteではすべてのコンポーネントからアクセスできるStoreを利用することでデータの共有を行うことができます。データの共有には状態管理と呼ばれ、本ブログでも紹介済みですがReactではRedux, Recoil, Zustand、Vue.jsではVuex, Piniaなどが利用されます。

Storeに保存されたデータを各コンポーネントでsubscribeすることでデータを取得することができます。subscribeという言葉がわかりにくい場合はaddEventListenerを思い出してください。addEventListenerを設定するとイベントを監視しイベントが発生したらあらかじめ設定していた処理を実行します。subscribeではデータの更新が行われると更新したデータを受け取ることができるのでそのデータを利用してあらかじめ設定した処理を実行することができます。

srcフォルダの下にstoresフォルダを作成しUserStore.jsファイルを作成します。svelte/storeからimportしたwritable関数の引数に初期値を設定しStoreの設定は完了です。


import { writable } from 'svelte/store';

const user = writable({
  name: 'John Doe',
  email: 'john@example.com',
});

export default user;

writable storeに保存されたデータを利用したいコンポーネントではwritable storeをimportしてsubscribeする必要があります。

AppコンポーネントでUserStoreをimportしてsubscribe関数でどのような値が取得できるか確認します。


<script>
  import UserStore from './stores/UserStore.js';

  const unsubscribe = UserStore.subscribe((value) => {
    console.log(value);
  });

  UserStore.subscribe((value) => {
    console.log(value);
  });
</script>

ブラウザでアクセスするとブラウザのデベロッパーツールのコンソールにvalueの値が表示されます。valueにはwritable関数に設定した初期値が表示されます。subscribeを実行することでwritable storeのデータが取得できることがわかります。


{
    "name": "John Doe",
    "email": "john@example.com"
}

subscribeして取得したデータは定義した変数userに保存します。userの中のnameプロパティをブラウザ上に表示することができます。


<script>
  import UserStore from './stores/UserStore.js';

  let user = '';

  UserStore.subscribe((value) => {
    user = value;
    console.log(value);
  });
</script>

<main>
  <h1>Store writable</h1>
  <div>{user.name}</div>
</main>

<style>
//略
</style>
storeに保存されたデータを表示
storeに保存されたデータを表示

subscribeを実行するとunsubscribe関数が戻されます。subscribeを停止するために必ずunsubscribe関数を実行する必要があります。アンマウント時にsubscribeを停止するためライフサイクルフックのonDestroyでunsubscribe関数を実行します。


<script>
  import { onDestroy } from 'svelte';
  import UserStore from './stores/UserStore.js';

  let user = '';

  const unsubscribe = UserStore.subscribe((value) => {
    user = value;
    console.log(value);
  });

  onDestroy(() => {
    unsubscribe();
  });
</script>
unsubscribeを設定せずコンポーネントのマウント、アンマウントを繰り返すとsubscribe関数が停止されず新しいsubscribe関数が実行されるためメモリリークにつながる可能性があります。

writable storeに新しい値を設定(set)

writable storeに新しい値を設定するためsetメソッドを利用することができます。ボタンをクリックすると新しいユーザ情報が設定されるようにsetNewUser関数を追加します。setNewUser関数ではsetメソッドで新しい値を設定しています。


<script>
  import { onDestroy } from 'svelte';
  import UserStore from './stores/UserStore.js';

  let user = '';

  const unsubscribe = UserStore.subscribe((value) => {
    user = value;
    console.log(value);
  });

  const setNewUser = () =>
    UserStore.set({
      name: 'Jane Doe',
      email: 'jane@example.com',
    });

  onDestroy(() => {
    unsubscribe();
  });
</script>

“New User”ボタンをクリックするとsubscribeの関数が実行されるのでユーザ情報が更新されコンソールには設定したユーザの情報が表示されます。

writable storeの値を更新(update)

writable storeに新しい値を設定するためにはupdateメソッドを利用します。ボタンをクリックするとユーザ名が更新されるようにupdateUser関数を追加します。updateUser関数ではupdateメソッドで新しい値を設定しています。


<script>
  import { onDestroy } from 'svelte';
  import UserStore from './stores/UserStore.js';

  let user = '';

  const unsubscribe = UserStore.subscribe((value) => {
    user = value;
    console.log(value);
  });

  const updateUser = () =>
    UserStore.update((user) => {
      return { ...user, name: 'Jane Doe' };
    });

  onDestroy(() => {
    unsubscribe();
  });
</script>

<main>
  <h1>Store writable</h1>
  <h2>{user.name}</h2>
  <button on:click={updateUser}>Update User</button>
</main>

updateメソッドでは引数のcallback関数の引数に現在のstoreのデータが渡されるので渡されたデータを更新して戻すことでstoreのデータが更新されます。

他のコンポーネントでsubscribe

Userコンポーネントでもwritable storeのデータにアクセスできるか確認するためにUser.svelteファイルに以下のコードを記述します。writable storeのデータにアクセスするためにはsubscribeを行う必要があります。


<script>
  import { onDestroy } from 'svelte';
  import UserStore from './stores/UserStore.js';

  let user = '';

  const unsubscribe = UserStore.subscribe((value) => {
    user = value;
  });
  onDestroy(() => {
    unsubscribe();
  });
</script>

<h2>User Email</h2>
<div>{user.email}</div>

作成したUserコンポーネントをAppコンポーネントでimportします。Update関数ではユーザ名からメールアドレスの更新に変更しています。


<script>
  import { onDestroy } from 'svelte';
  import UserStore from './stores/UserStore.js';
  import User from './User.svelte';

  let user = '';

  const unsubscribe = UserStore.subscribe((value) => {
    user = value;
    console.log(value);
  });

  const updateUser = () =>
    UserStore.update((user) => {
      return { ...user, email: 'jane@example.com' };
    });

  onDestroy(() => {
    unsubscribe();
  });
</script>

<main>
  <h1>Store writable</h1>
  <h2>{user.name}</h2>
  <button on:click={updateUser}>Update User</button>
  <User />
</main>

<style>
//略
Userコンポーネントでstoreのデータを表示
Userコンポーネントでstoreのデータを表示

Appコンポーネントで設定している”Update User”ボタンをクリックするとUserコンポーネントで表示しているメールアドレスが更新されること(john@exmaple.com→jane@examle.com)が確認できます。複数のコンポーネントをまたがってデータを共有できていることが確認できました。

メールアドレスの更新
メールアドレスの更新

$の利用

App, Userコンポーネントではstoreのデータにアクセスするためには必ずsubscribeとunsubscribeの設定が必要であることがわかりました。Svelteはsubscribeとunsubscribeを管理する機能を備えているいます。その機能を利用するためにはimportしたstoreの名前の前に$(ダラー)をつけます。

Userコンポーネントであれば下記のように記述することができます。importしたUserStoreの名前をuserに変更しています。UserStoreのままでも構いません。コードがスッキリしていることがわかります。


<script>
  import user from './stores/UserStore.js';
</script>

<h2>User Email</h2>
<div>{$user.email}</div>

Appコンポーネントも$を利用して書き換えます。$に利用することでupdateUser関数も書き換えることができます。updateメソッドをそのまま利用することも可能です。


<script>
  import user from './stores/UserStore.js';
  import User from './User.svelte';

  const updateUser = () => {
    $user.email = 'jane@example.com';
  };

    // updateメソッドも利用可能
  // const updateUser = () =>
  //   user.update((user) => {
  //     return { ...user, email: 'jane@example.com' };
  //   });
</script>

<main>
  <h1>Store writable</h1>
  <h2>{$user.name}</h2>
  <button on:click={updateUser}>Update User</button>
  <User />
</main>

setメソッドも下記のように書き換えが可能です。


  const setNewUser = () =>
    ($user = { name: 'Jane Doe', email: 'jane@example.com' });

  // setメソッドも利用可能
  // const setNewUser = () =>
  //   UserStore.set({
  //     name: 'Jane Doe',
  //     email: 'jane@example.com',
  //   });

まとめ

Svelteの基本的な機能を確認することができました。今後はSvelteによるアプリケーションをより効率的に行うためにSvelte Kitなど学習してさらにSvelteの理解を深める必要があります。