CheatSheet
日本語 icon日本語English iconEnglish
チートシートとはカンニングペーパーのことです。それが転じて、本来覚えることをまとめておいたものです。
要点をすぐに参照できるようにまとめてみました。

Svelte

エンジニアのためのWebチートシート

Svelteはコンパイラベースのフロントエンドフレームワークです。 Svelte 5のRunes($state, $derived, $effect等)による新しいリアクティビティシステムを中心に、テンプレート構文、バインディング、トランジション、SvelteKitの基本をチートシートにまとめました。

コンポーネント基本

コンポーネント構造

  • script(ロジック)、マークアップ(HTML)、style(CSS)の3セクションで構成されます。

    <script>
      let count = $state(0);
    
      function increment() {
        count++;
      }
    </script>
    
    <button onclick={increment}>
      Count: {count}
    </button>
    
    <style>
      button {
        font-size: 1.2rem;
        padding: 0.5rem 1rem;
      }
    </style>

TypeScript対応

  • lang="ts" でTypeScriptを使用できます。

    <script lang="ts">
      let name: string = $state('World');
      let count: number = $state(0);
    
      interface User {
        id: number;
        name: string;
      }
      let user: User = $state({ id: 1, name: 'Alice' });
    </script>
    
    <h1>Hello {name}!</h1>

スコープドCSS

  • スタイルはコンポーネントにスコープされます。:global() でグローバルスタイルを適用できます。

    <style>
      /* Scoped to this component */
      p { color: blue; }
    
      /* Global style */
      :global(body) {
        margin: 0;
      }
    
      /* Nested global */
      div :global(strong) {
        color: red;
      }
    </style>

Runes(ルーン)

$state - リアクティブ状態

  • リアクティブな状態を宣言します。オブジェクト・配列はディープリアクティブです。

    let count = $state(0);
    
    // Object / Array (deep reactivity)
    let user = $state({ name: 'Alice', age: 30 });
    user.name = 'Bob'; // auto-updates UI
    let items = $state(['Apple', 'Banana']);
    items.push('Cherry'); // auto-updates UI
    // $state.raw (no deep reactivity)
    let data = $state.raw({ name: 'Alice' });
    data.name = 'Bob';        // NOT reactive
    data = { name: 'Bob' };   // reactive
    
    // $state.snapshot (get plain object)
    let obj = $state({ count: 0 });
    console.log($state.snapshot(obj));
    // In classes
    class Todo {
      done = $state(false);
      text = $state('');
      constructor(text) { this.text = text; }
      toggle() { this.done = !this.done; }
    }

$derived - 派生状態

  • 他の状態から自動計算される値です。メモ化され、依存値が変わった時のみ再計算されます。

    let count = $state(0);
    let doubled = $derived(count * 2);
    let isEven = $derived(count % 2 === 0);
    
    // $derived.by (complex computation)
    let total = $derived.by(() => {
      let sum = 0;
      for (const n of numbers) {
        sum += n;
      }
      return sum;
    });

$effect - 副作用

  • マウント時と依存値の変更時に実行されます。クリーンアップ関数を返せます。

    // Runs on mount + dependency change
    $effect(() => {
      console.log(`Count: ${count}`);
    
      // Cleanup (before re-run / on destroy)
      return () => {
        console.log('cleanup');
      };
    });
    
    // $effect.pre (before DOM update)
    $effect.pre(() => {
      // e.g., save scroll position
    });

$props - プロパティ

  • 親コンポーネントから渡されるプロパティを受け取ります。

    <!-- Child component -->
    <script lang="ts">
      interface Props {
        name: string;
        count?: number;
        class?: string;
      }
      let {
        name, count = 0, class: klass, ...rest
      } = $props<Props>();
    </script>
    <div class={klass} {...rest}>
      <p>{name}: {count}</p>
    </div>
    <!-- Parent component -->
    <MyComponent name="Alice" count={5} />

$bindable & $inspect

  • $bindableは双方向バインディング可能なプロパティ、$inspectはデバッグ専用のルーンです。

    <!-- $bindable: two-way binding prop -->
    <script>
      let { value = $bindable('') } = $props();
    </script>
    <input bind:value={value} />
    
    <!-- Parent: bind:value -->
    <FancyInput bind:value={message} />
    
    <!-- $inspect: debug only (dev mode) -->
    <script>
      $inspect(count, message);
      $inspect(count).with((type, val) => {
        if (type === 'update') debugger;
      });
    </script>

テンプレート構文

{#if} & {#each}

  • 条件分岐とリストレンダリングです。

    {#if temperature > 100}
      <p>too hot!</p>
    {:else if temperature < 80}
      <p>too cold!</p>
    {:else}
      <p>just right!</p>
    {/if}
    {#each items as item}
      <li>{item.name}</li>
    {/each}
    
    {#each items as item, i}
      <li>{i + 1}: {item.name}</li>
    {/each}
    
    {#each items as item (item.id)}
      <li>{item.name}</li>
    {/each}
    {#each items as { id, name, qty }, i (id)}
      <li>{name} x {qty}</li>
    {/each}
    
    {#each todos as todo}
      <p>{todo.text}</p>
    {:else}
      <p>No tasks today!</p>
    {/each}

{#await} & {#key}

  • Promise処理と値変更時の再生成です。

    {#await promise}
      <p>Loading...</p>
    {:then value}
      <p>Result: {value}</p>
    {:catch error}
      <p>Error: {error.message}</p>
    {/await}
    
    {#await promise then value}
      <p>Result: {value}</p>
    {/await}
    {#key value}
      <Component />
    {/key}
    
    {#key value}
      <div transition:fade>{value}</div>
    {/key}
    
    {@const total = item.price * item.qty}
    {@html '<strong>Bold</strong>'}

{#snippet} & {@render}

  • 再利用可能なテンプレート片の定義とレンダリングです(Svelte 5でslotを置換)。

    {#snippet greeting(name)}
      <p>Hello, {name}!</p>
    {/snippet}
    {@render greeting('World')}
    
    <Table data={fruits}>
      {#snippet header()}
        <th>Name</th><th>Qty</th>
      {/snippet}
      {#snippet row(item)}
        <td>{item.name}</td><td>{item.qty}</td>
      {/snippet}
    </Table>
    <!-- children snippet (Card.svelte) -->
    <script>
      let { children } = $props();
    </script>
    <div class="card">
      {@render children?.()}
    </div>
    
    <Card><p>Card content here</p></Card>
    {#if children}
      {@render children()}
    {:else}
      <p>Fallback content</p>
    {/if}

イベント & バインディング

イベントハンドリング

  • Svelte 5ではHTML標準のイベント属性(onclick等)を使用します。

    <button onclick={handleClick}>Click</button>
    <button onclick={() => count++}>+1</button>
    
    <!-- Shorthand (same name) -->
    <script>
      function onclick() { count++; }
    </script>
    <button {onclick}>Click</button>
    <!-- Modifiers (Svelte 5 style) -->
    <script>
      function handleSubmit(event) {
        event.preventDefault();
      }
    </script>
    <form onsubmit={handleSubmit}>...</form>
    
    <!-- Capture phase -->
    <button onclickcapture={handler}>
      Capture
    </button>

バインディング

  • bind:ディレクティブによるフォーム要素やDOM参照への双方向バインディングです。

    <input bind:value={name} />
    <input type="number" bind:value={count} />
    <input type="range" bind:value={vol} />
    <input type="checkbox" bind:checked={ok} />
    <textarea bind:value={content}></textarea>
    <!-- Radio group -->
    {#each ['A', 'B', 'C'] as opt}
      <label>
        <input type="radio" bind:group={sel}
          value={opt} /> {opt}
      </label>
    {/each}
    
    <!-- Select -->
    <select bind:value={selected}>
      {#each options as opt}
        <option value={opt.id}>{opt.name}</option>
      {/each}
    </select>
    <canvas bind:this={canvasEl}></canvas>
    
    <div bind:clientWidth={w}
         bind:clientHeight={h}>
      {w} x {h}
    </div>

コンポーネントイベント

  • Svelte 5ではコールバックプロパティでイベントを伝播します。

    <!-- Child: callback prop -->
    <script>
      let { onsubmit } = $props();
    </script>
    <button onclick={() => onsubmit({
      text: 'Hello'
    })}>Submit</button>
    
    <!-- Parent -->
    <ChildComponent onsubmit={(data) => {
      console.log(data.text);
    }} />
    <!-- Spread events from parent -->
    <script>
      let { onclick, onhover, ...rest } = $props();
    </script>
    <button {onclick} {...rest}>Click me</button>

トランジション & アニメーション

トランジションの使い方

  • 要素の出入りにアニメーションを適用します。

    <script>
      import { fade, fly, slide, scale }
        from 'svelte/transition';
      let visible = $state(true);
    </script>
    
    {#if visible}
      <div transition:fade>Fades</div>
      <div transition:fly={{ y: 200, duration: 500 }}>
        Flies
      </div>
      <div transition:slide={{ axis: 'x' }}>
        Slides
      </div>
    {/if}
    {#if visible}
      <div in:fly={{ y: -200 }} out:fade>
        Different in/out
      </div>
    {/if}
    
    <div
      transition:fly={{ y: 200 }}
      onintrostart={() => status = 'intro'}
      onoutroend={() => status = 'done'}
    >Content</div>

組み込みトランジション一覧

トランジション効果主要パラメータ
fade不透明度delay, duration
blurぼかし + 不透明度amount, duration
fly移動 + 不透明度x, y, duration
slideスライドaxis, duration
scale拡大縮小start, duration
drawSVG描画speed, duration
crossfade要素間の遷移fallback, duration

アニメーション(keyed each内)

  • {#each}でキーを使う場合、要素の並べ替えをアニメーションできます。

    <script>
      import { flip } from 'svelte/animate';
    </script>
    
    {#each items as item (item.id)}
      <div animate:flip={{ duration: 300 }}>
        {item.name}
      </div>
    {/each}

ストア & 状態共有

共有状態(Svelte 5推奨)

  • .svelte.js ファイルでrunesを使い、コンポーネント間で状態を共有します。

    // shared-state.svelte.js
    export const counter = $state({ count: 0 });
    
    export function increment() {
      counter.count++;
    }
    
    export function reset() {
      counter.count = 0;
    }
    <!-- Any component -->
    <script>
      import { counter, increment }
        from './shared-state.svelte.js';
    </script>
    
    <button onclick={increment}>
      Count: {counter.count}
    </button>

従来のストア

  • writable/readable/derivedストアは引き続き利用可能です。

    import { writable, readable, derived }
      from 'svelte/store';
    
    const count = writable(0);
    count.set(1);
    count.update(n => n + 1);
    
    const doubled = derived(
      count, $count => $count * 2
    );
    const time = readable(new Date(), (set) => {
      const interval = setInterval(
        () => set(new Date()), 1000
      );
      return () => clearInterval(interval);
    });
    <!-- Auto subscribe with $ prefix -->
    <script>
      import { count } from './stores.js';
    </script>
    <p>Count: {$count}</p>
    <button onclick={() => $count++}>+1</button>

特別な要素 & ディレクティブ

特別な要素

  • svelte:window、svelte:head 等のシステム要素です。

    <svelte:window
      onkeydown={handleKeydown}
      bind:scrollY={y}
      bind:innerWidth={w}
    />
    <svelte:document
      onvisibilitychange={handleVisibility}
    />
    <svelte:body onmouseenter={handleEnter} />
    <svelte:head>
      <title>My Page</title>
      <meta name="description" content="..." />
    </svelte:head>
    
    <svelte:element this={tag}>Content</svelte:element>
    <svelte:component this={component} />

class & style ディレクティブ

  • 条件付きクラスとインラインスタイルの設定です。

    <div class:active={isActive}>Conditional</div>
    <div class:active>Shorthand</div>
    
    <!-- Object syntax (v5.16+) -->
    <div class={{
      active: isActive,
      bold: isBold
    }}>Multiple classes</div>
    <div style:color={textColor}>Styled</div>
    <div style:--custom="value">CSS var</div>
    <div style:transform={
      flipped ? 'rotateY(0)' : 'rotateY(180deg)'
    }>Flip</div>
    
    <button disabled={!isValid}>Submit</button>

SvelteKit ルーティング

ファイルベースルーティング

  • src/routes/ ディレクトリ構造でルーティングが決まります。

    src/routes/
    ├── +page.svelte         → /
    ├── +layout.svelte       → shared layout
    ├── +error.svelte        → error page
    ├── about/+page.svelte   → /about
    ├── blog/
    │   ├── +page.svelte     → /blog
    │   ├── +page.server.ts  → server load
    │   └── [slug]/
    │       ├── +page.svelte → /blog/:slug
    │       └── +page.ts     → universal load
    ├── api/posts/
    │   └── +server.ts       → /api/posts
    └── (auth)/              → route group
        ├── +layout.svelte
        ├── login/+page.svelte
        └── register/+page.svelte

レイアウト

  • 共通レイアウトの定義です。children スニペットで子ページを挿入します。

    <!-- +layout.svelte -->
    <script>
      let { children } = $props();
    </script>
    
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
    </nav>
    
    <main>
      {@render children()}
    </main>
    
    <footer>Footer</footer>

SvelteKit データ & フォーム

ロード関数

  • サーバー/クライアントでのデータ取得です。

    // +page.ts (universal: server + client)
    import type { PageLoad } from './$types';
    
    export const load: PageLoad = async ({
      params, fetch
    }) => {
      const res = await fetch(
        `/api/posts/${params.slug}`
      );
      return { post: await res.json() };
    };
    // +page.server.ts (server only)
    import type { PageServerLoad } from './$types';
    import { db } from '$lib/server/database';
    
    export const load: PageServerLoad = async ({
      params, cookies
    }) => {
      const post = await db.getPost(params.slug);
      return { post };
    };
    <!-- +page.svelte -->
    <script>
      let { data } = $props();
    </script>
    <h1>{data.post.title}</h1>
    <p>{data.post.content}</p>

フォームアクション

  • サーバーサイドのフォーム処理です。プログレッシブエンハンスメント対応。

    // +page.server.ts
    import { fail, redirect } from '@sveltejs/kit';
    
    export const actions = {
      login: async ({ request, cookies }) => {
        const data = await request.formData();
        const user = await authenticate(
          data.get('email'), data.get('password')
        );
        if (!user) {
          return fail(401, { incorrect: true });
        }
        cookies.set('session', user.token,
          { path: '/' });
        throw redirect(303, '/dashboard');
      }
    };
    <!-- +page.svelte -->
    <script>
      import { enhance } from '$app/forms';
      let { form } = $props();
    </script>
    <form method="POST" action="?/login"
      use:enhance>
      <input name="email"
        value={form?.email ?? ''} />
      {#if form?.incorrect}
        <p class="error">Invalid credentials</p>
      {/if}
      <button>Log in</button>
    </form>

APIルート

  • +server.ts でREST APIエンドポイントを作成します。

    // src/routes/api/posts/+server.ts
    import { json } from '@sveltejs/kit';
    import type { RequestHandler } from './$types';
    
    export const GET: RequestHandler = async ({
      url
    }) => {
      const limit = Number(
        url.searchParams.get('limit') ?? 10
      );
      return json(await db.getPosts(limit));
    };
    export const POST: RequestHandler = async ({
      request
    }) => {
      const data = await request.json();
      const post = await db.createPost(data);
      return json(post, { status: 201 });
    };