📑

Svelteチュートリアルやってみた Part1 Basic

2023/07/05に公開

概要

  • Svelteのメジャーバージョンが上がったのでそろそろ触っていきたいなという興味本位から実施。
  • ReactのServer Componentsの登場だったりでReactの優位性は高くなってきていると思うが、別のフレームワークの状況はどうなっているのか興味が湧いた。
  • チュートリアルで「なるほど」となった点、詰まったところをまとめています。
    • 公式Docsを読むのが一番なので、気になった人は公式チュートリアルへGO。
  • フロントエンド全然わからんマンなので、言葉の使い方が違っているケースは多々あると思われます。
    • コメントもらえるとうれしいです(´・ω・`)

※幅広く触ってきている人が気になる点を記載しているイメージです。

基本的な記述〜reactiveな値について

JSXなどは利用せず、Vue風な記述方法。

Svelteは双方向バインディングらしい。
reactiveな宣言をする際には、値を代入するケースと、そのまま直書きするケースがある。
以下の例だと、countがreactiveな値となるので、the count is ${count}というログがクリックするたび流れていく。
もちろん別の変数へ代入させることも可能。

<script>
	let count = 0;
	let tmp = 0;
	$: console.log(`the count is ${count}`);
	$: console.log(`the tmp is ${tmp}`);
	$: doubled = count * 2; 別変数へ代入するケース

	function handleClick() {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

if文について

if文は以下のように書ける。
どういう順序で動くのかを確認してみたところ、値として持たせている箇所が動く。

<script>
	let count = 0;

	$: if (count >= 10) {
		alert('count is high!');
		// count = 0;
	}
	
	$: if (count >= 10) {
		alert('count is dangerously high!');  // こっちが動く
		count = 0;
	}

	function handleClick() {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

arrayのreactiveについて

arrayをreactiveにするためには、以下のような記載が必要らしい。
reactiveな変数として宣言した後に、代入をするパターンと、Reactのようなスプレッド演算子一発で代入するパターンとが紹介されていた。

<script>
	let numbers = [1, 2, 3, 4];

	function addNumber() {
		numbers.push(numbers.length + 1);
		numbers = numbers;  // 事前に変数を処理して、変数代入を明示するパターン。
		numbers = [...numbers,numbers.length + 1]; // Reactのようなスプレッド演算子でもOK。
	}

	$: sum = numbers.reduce((t, n) => t + n, 0);
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}>
	Add a number
</button>

コンポーネントのHTML部分

テンプレートエンジンの書き方に近いですかね。
jsxゴリゴリの後に見ると新鮮さを感じる(´・ω・`)

<script>
	let count = 0;

	function increment() {
		count += 1;
	}
</script>

<button on:click={increment}>
	Clicked {count}
	{count === 1 ? 'time' : 'times'}
</button>

// ここから下の部分
{#if count > 10}
	<p>{count} is greater than 10</p>
{:else if count < 5}
	<p>{count} is less than 5</p>
{:else}
	<p>{count} is between 5 and 10</p>
{/if}

ブロックの区切り

#はブロック開始
:はブロック継続
/はブロックの終了を示唆するらしいです(´・ω・`)

loopについて

よくあるforeachで回すやり方がある。
インデックスも付けれるので安心ですね。

App.svelte
<script>
	const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
	let selected = colors[0];
</script>

<h1 style="color: {selected}">Pick a colour</h1>

<div>
	{#each colors as color, i}
		<button
			aria-current={selected === color}
			aria-label={color}
			style="background: {color}"
			on:click={() => selected = color}
		>{i + 1}</button>
	{/each}
</div>

非同期処理について

非同期の返り値をreactiveな変数として定義しておいて、それをテンプレート側で使えばOK。
※promiseがreactiveなものとして宣言され、ボタンクリックでイベント実行〜promiseへ代入まで行われる。

かなりシンプルに見れるのでいい感じ。

App.svelte
<script>
	import { getRandomNumber } from './utils.js';

	let promise = getRandomNumber();

	function handleClick() {
		promise = getRandomNumber();
	}
</script>

<button on:click={handleClick}>
	generate random number
</button>

{#await promise}
	<p>...waiting</p>
{:then number}
	<p>The number is {number}</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}
utils.js
export async function getRandomNumber() {
    // Fetch a random number between 0 and 100
    // (with a delay, so that we can see it)
    const res = await fetch('/random-number');

    if (res.ok) {
        return await res.text();
    } else {
        // Sometimes the API will fail!
        throw new Error('Request failed');
    }
}

eventの伝搬

component間でのeventの受け渡しは、特定の関数を利用する。
この場合だと、event名はmessageとして実施している。ここは任意のものに変更可能。(Docsだとgreetに変えてみてねって書いている)

on:messageの部分が該当箇所。
dispatch()の引数にイベント名を指定する必要がある。

App
<script>
	import Inner from './Inner.svelte';

	function handleMessage(event) {
		alert(event.detail.text);
	}
</script>

<Inner on:message={handleMessage} />
Inner
<script>
	import { createEventDispatcher } from 'svelte';

	const dispatch = createEventDispatcher();

	function sayHello() {
		dispatch('message', {
			text: 'Hello!'
		});
	}
</script>

<button on:click={sayHello}>
	Click to say hello
</button>

仮に複数コンポーネントに跨いでeventを渡していきたい場合には、以下のように記述することで、createEventDispatcher()をわざわざ書かずに省略できる。
App→Outer→Innerというコンポーネントの順番でmessageを伝搬させていきたい場合の例。

outer.js
<Inner on:message />

bindについて

bindで思考がフリーズしてしまった。なにこれ...感覚的にわかんない...。
個人的な感覚としては、eventのvalueがreactive化するってことですね(´・ω・`)
Svelte側でいい感じにラップしてくれてるんでしょう。

textareaタグでもbind:value={hoge}のようにしているので、内部的な動作は一旦置いておき、記述方法としてはそういうことらしい。
※実際に使用するときにはもう少し詳細に調べたいところですね。

App.svelte
<script>
	let questions = [
		{
			id: 1,
			text: `Where did you go to school?`
		},
		{
			id: 2,
			text: `What is your mother's name?`
		},
		{
			id: 3,
			text: `What is another personal fact that an attacker could easily find with Google?`
		}
	];

	let selected;

	let answer = '';

	function handleSubmit() {
		alert(
			`answered question ${selected.id} (${selected.text}) with "${answer}"`
		);
	}
</script>

<h2>Insecurity questions</h2>

<form on:submit|preventDefault={handleSubmit}>
    <select
        bind:value={selected}   // ここがわかりにくかった箇所。selectで選択されたvalueがbindされる。
		on:change={() => (answer = '')}
	>
		{#each questions as question}
			<option value={question}>
				{question.text}
			</option>
		{/each}
	</select>

	<input bind:value={answer} />

	<button disabled={!answer} type="submit">
		Submit
	</button>
</form>

<p>
	selected question {selected
		? selected.id
		: '[waiting...]'}
</p>

Lifecycleについて

LIfecycleは触ってみるのが一番手っ取り早そう。
チュートリアルを見る範囲だけだと、あんまりつまづく箇所はなかった。
いざ触ってみるとギエエエエってなる気がするけど一旦は置いておきましょう。

storeについて

storeの定義

めちゃくちゃ簡単に書けるんです。これは楽ちん。
まず、以下の様にstoreを定義してexportしてあげる。

store.js
import { writable } from 'svelte/store';

export const count = writable(0);

storeの呼び出し

storeで定義した変数については、以下の様に${count}で参照できる。
グローバルなstateはごちゃついてしまう傾向が多いけど、Svelte側でここまでシンプルにしてくれるのは助かる(´・ω・`)

App.svelte
<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Decrementer from './Decrementer.svelte';
	import Resetter from './Resetter.svelte';
</script>

<h1>The count is {$count}</h1>

<Incrementer />
<Decrementer />
<Resetter />

Svelteにおいて、$はstoreであることを示唆するとのこと。

Any name beginning with $ is assumed to refer to a store value. It's effectively a reserved character — Svelte will prevent you from declaring your own variables with a $ prefix.

storeをグローバルに呼び出す注意点

storeを呼び出す際には、トップレベルコンポーネントで呼び出しておく必要がある。
もしくは、呼び出すコンポーネントより上位のコンポーネントで呼び出せばOK?(未検証)

Auto-subscription only works with store variables that are declared (or imported) at the top-level scope of a component.

手動でのunsubscribeすることも可能だが、auto-subscribeに任せる様にしたいですね。
Lifecycleについてはもう少し掘り下げてパフォーマンスとの関連性も認識する必要がありそう。
https://learn.svelte.dev/tutorial/auto-subscriptions

Custom stores

以下の記述が面白かった。subscribe強すぎる(´・ω・`)
カスタムフックに慣れていれば問題なさそう。
慣れていない場合はこっちの方が感覚的にわかりそうなイメージ。

As long as an object correctly implements the subscribe method, it's a store. Beyond that, anything goes. It's very easy, therefore, to create custom stores with domain-specific logic.

store.js
import { writable } from 'svelte/store';

function createCount() {
	const { subscribe, set, update } = writable(0);

	return {
		subscribe,
		increment: () => {update((n) => n + 1)},
		decrement: () => {update((n) => n - 1)},
		reset: () => {set(0)}
	};
}

export const count = createCount();

store まとめ

  • storeについて、全体的に感覚的にわかりやすいなあという印象。
  • storeをeventにbindすることもできるので、色んなところから簡単にstoreを変更できるのはいいかも。
    • 一瞬で無秩序になりそうなので、実使用時にはちゃんと設計しましょう...(´・ω・`)

まとめ

公式チュートリアルのBasicから気になった点を抜粋してまとめてみました。
ざっくり所感としては学習コストは低いだろうと思います。フロントエンド専任の人間でなくともさくさくチュートリアルは進みました。
そういう面から見て、採用理由に至るケースは増えるのではないでしょうか。エコシステムだったり、SvelteKitの使用感は気になるところですが。

個人的に興味のある領域なのでAdvance以降もまとめようかなと思います。

ちなみにスベルテじゃなくてスベルトだからね!気をつけようね!(´・ω・`)

GitHubで編集を提案

Discussion