Svelte入門:リファレンス $app/stores -Vol.2-

スポンサーリンク
Svelte入門:リファレンス $app/stores -Vol.2- 用語解説
Svelte入門:リファレンス $app/stores -Vol.2-
この記事は約29分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

今日は、SvelteKitのリファレンスについて解説しています。

スポンサーリンク

背景

SvelteKitのリファレンスについて調査する機会がありましたので、その時の内容を備忘として記事に残しました。

$app/stores

このモジュールには、$app/state からのエクスポートに相当するストアベースの機能が含まれています。SvelteKit 2.12 以降を使用している場合は、代わりに $app/state モジュールを使用してください。

import { getStores, navigating, page, updated } from '$app/stores';

$app/stores モジュールは SvelteKit の以前のバージョンで使用されていたモジュールで、現在のページ情報やナビゲーション状態にアクセスするための Svelte ストアを提供します。SvelteKit 2.12 以降では、代わりに $app/state モジュールを使用することが推奨されています。

このモジュールには以下の主要なエクスポートが含まれています:

  1. getStorespagenavigatingupdated ストアを含むオブジェクトを返す関数です。通常は直接インポートした方が便利ですが、関数内で使用する場合など、特定のケースで役立ちます。
  2. page – 現在のページに関する情報(URL、パラメータ、データなど)を含む読み取り専用ストアです。
  3. navigating – 進行中のナビゲーションに関する情報を含むストア、またはナビゲーションが発生していない場合は null です。
  4. updated – アプリケーションの新しいバージョンが利用可能かどうかを示すストアです。

navigating

代わりに $app/state からの navigating を使用してください(Svelte 5 が必要、詳細はドキュメントを参照)

読み取り専用ストアです。ナビゲーションが開始されると、その値は fromtotype、および(type === 'popstate' の場合)delta プロパティを持つ Navigation オブジェクトになります。ナビゲーションが終了すると、その値は null に戻ります。

サーバー上では、このストアはコンポーネントの初期化中にのみ購読できます。ブラウザでは、いつでも購読することができます。

const navigating: import('svelte/store').Readable
	import('@sveltejs/kit').Navigation | null
>;

解説

navigating は SvelteKit の $app/stores モジュールが提供するストアで、以下のような特徴があります:

  1. ナビゲーション状態の追跡 – アプリケーション内でのナビゲーションの進行状況を追跡します。
  2. 読み取り専用ストア – Svelte の Readable ストアとして実装されており、外部から値を設定することはできません。
  3. 値の変化:
    • ナビゲーション中: Navigation オブジェクト(詳細は以下を参照)
    • ナビゲーションなし: null
  4. Navigation オブジェクトのプロパティ:
    • from – ナビゲーション元のページに関する情報
    • to – ナビゲーション先のページに関する情報
    • type – ナビゲーションのタイプ(linkpopstategoto など)
    • deltatype === 'popstate' の場合の履歴スタックの変化量
  5. 使用上の注意 – SvelteKit 2.12 以降では、$app/state モジュールからの navigating の使用が推奨されています。これには Svelte 5 が必要です。

使用例

<script>
  import { navigating } from '$app/stores';
  
  // ナビゲーション中かどうかを追跡
  $: isLoading = $navigating !== null;
  
  // ナビゲーション情報をログに記録
  $: {
    if ($navigating) {
      console.log('ナビゲーション開始:');
      console.log(`元: ${$navigating.from.url.pathname}`);
      console.log(`先: ${$navigating.to.url.pathname}`);
      console.log(`タイプ: ${$navigating.type}`);
      
      if ($navigating.type === 'popstate') {
        console.log(`デルタ: ${$navigating.delta}`);
      }
    }
  }
</script>

<!-- ナビゲーション中のローディングインジケータ -->
{#if isLoading}
  <div class="loading-overlay">
    <div class="spinner"></div>
    <p>
      {$navigating.from.url.pathname} から {$navigating.to.url.pathname} へ移動中...
    </p>
  </div>
{/if}

<style>
  .loading-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(255, 255, 255, 0.8);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 1000;
  }
  
  .spinner {
    width: 50px;
    height: 50px;
    border: 5px solid #f3f3f3;
    border-top: 5px solid #3498db;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin-bottom: 1rem;
  }
  
  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
</style>

使用例:ナビゲーションタイプに基づいたトランジション

<script>
  import { navigating } from '$app/stores';
  import { fade, fly, slide } from 'svelte/transition';
  import { onMount } from 'svelte';
  
  let previousNav = null;
  let transitionType = null;
  
  // ナビゲーションタイプを記録して適切なトランジションを設定
  onMount(() => {
    const unsubscribe = navigating.subscribe(nav => {
      if (nav !== null) {
        // ナビゲーション開始時
        previousNav = nav;
        
        // ナビゲーションタイプに基づいてトランジションを設定
        if (nav.type === 'link') {
          transitionType = 'fade';
        } else if (nav.type === 'popstate') {
          transitionType = nav.delta > 0 ? 'slide-back' : 'slide-forward';
        } else {
          transitionType = 'fly';
        }
      }
    });
    
    return unsubscribe;
  });
</script>

<main>
  <!-- 通常のコンテンツ -->
  <slot />
</main>

<!-- ナビゲーション中のオーバーレイ、トランジションタイプに基づいて異なるアニメーション -->
{#if $navigating}
  <div class="nav-overlay">
    {#if transitionType === 'fade'}
      <div transition:fade={{ duration: 300 }}>
        ページ移動中...
      </div>
    {:else if transitionType === 'slide-back'}
      <div transition:slide={{ duration: 300 }}>
        前のページに戻ります...
      </div>
    {:else if transitionType === 'slide-forward'}
      <div transition:slide={{ duration: 300 }}>
        次のページに進みます...
      </div>
    {:else}
      <div transition:fly={{ y: 20, duration: 300 }}>
        移動中...
      </div>
    {/if}
  </div>
{/if}

新しい $app/state モジュールへの移行

SvelteKit 2.12 以降および Svelte 5 を使用している場合は、以下のように $app/state モジュールを使用するようにコードを更新することをお勧めします:

<script>
  // $app/stores からのインポートを置き換える
  // import { navigating } from '$app/stores';
  
  // 代わりに $app/state からインポート
  import { navigating } from '$app/state';
  
  // ルーンを使用したリアクティブな値
  const isLoading = $derived(navigating !== null);
  const navFrom = $derived(navigating?.from?.url.pathname);
  const navTo = $derived(navigating?.to?.url.pathname);
</script>

{#if isLoading}
  <div class="loading">
    {navFrom} から {navTo} へ移動中...
  </div>
{/if}

navigating ストアは、ページ遷移の検出、ローディングインジケータの表示、ナビゲーションタイプに基づいたアニメーションの適用など、ユーザーエクスペリエンスを向上させるための多くのシナリオで非常に役立ちます。

page

代わりに $app/state からの page を使用してください(Svelte 5 が必要、詳細はドキュメントを参照)

ページデータを含む値を持つ読み取り専用ストアです。 サーバー上では、このストアはコンポーネントの初期化中にのみ購読できます。ブラウザでは、いつでも購読することができます。

const page: import('svelte/store').Readable
	import('@sveltejs/kit').Page
>;

解説

page は SvelteKit の $app/stores モジュールが提供するストアで、以下のような特徴があります:

  1. ページ情報の提供 – 現在のページに関する様々な情報にアクセスするための読み取り専用ストアです。
  2. Page オブジェクトのプロパティ:
    • url – 現在のページの URL 情報(pathnamesearchhash など)
    • params – ルートパラメータの値
    • route – 現在のルートの ID
    • status – HTTP ステータスコード
    • error – エラーがある場合はそのオブジェクト、なければ null
    • data – すべての load 関数から返されたデータを組み合わせたオブジェクト
    • form – フォームアクションから返されたデータ
    • state – ナビゲーション状態(gotopushStatereplaceState で設定)
  3. 読み取り専用 – Svelte の Readable ストアとして実装されており、外部から値を設定することはできません。
  4. 使用上の注意 – SvelteKit 2.12 以降では、$app/state モジュールからの page の使用が推奨されています。これには Svelte 5 が必要です。

使用例

<script>
  import { page } from '$app/stores';
  
  // URL 情報へのアクセス
  $: pathname = $page.url.pathname;
  $: searchParams = $page.url.searchParams;
  $: hash = $page.url.hash;
  
  // ルートパラメータへのアクセス
  $: routeParams = $page.params;
  
  // ページデータへのアクセス
  $: userData = $page.data.user;
  $: posts = $page.data.posts;
  
  // エラー処理
  $: hasError = $page.error !== null;
  
  // アクティブなナビゲーションアイテムの判定
  $: isHomeActive = pathname === '/';
  $: isAboutActive = pathname === '/about';
  $: isBlogActive = pathname.startsWith('/blog');
  
  // クエリパラメータの取得
  $: query = searchParams.get('q') || '';
  $: sortBy = searchParams.get('sort') || 'date';
</script>

<header>
  <h1>現在のページ: {pathname}</h1>
  
  <nav>
    <ul>
      <li class:active={isHomeActive}>
        <a href="/">ホーム</a>
      </li>
      <li class:active={isAboutActive}>
        <a href="/about">会社概要</a>
      </li>
      <li class:active={isBlogActive}>
        <a href="/blog">ブログ</a>
      </li>
    </ul>
  </nav>
</header>

<!-- パラメータの表示 -->
{#if Object.keys(routeParams).length > 0}
  <div class="params">
    <h2>ルートパラメータ:</h2>
    <pre>{JSON.stringify(routeParams, null, 2)}</pre>
  </div>
{/if}

<!-- ページデータの表示 -->
{#if userData}
  <div class="user-profile">
    <img src={userData.avatar} alt="プロフィール画像" />
    <h3>{userData.name}</h3>
    <p>{userData.email}</p>
  </div>
{/if}

<!-- エラー表示 -->
{#if hasError}
  <div class="error">
    <h2>エラーが発生しました</h2>
    <p>ステータスコード: {$page.status}</p>
    <p>メッセージ: {$page.error.message}</p>
  </div>
{/if}

<!-- クエリパラメータの使用 -->
{#if query}
  <p>検索クエリ: {query}</p>
{/if}

<!-- ソート順の表示 -->
<p>ソート順: {sortBy}</p>

<style>
  .active {
    font-weight: bold;
    text-decoration: underline;
  }
  
  .params {
    background: #f5f5f5;
    padding: 1rem;
    border-radius: 4px;
    margin: 1rem 0;
  }
  
  .user-profile {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    margin: 1rem 0;
  }
  
  .error {
    background: #fff0f0;
    border: 2px solid #ff0000;
    padding: 1rem;
    border-radius: 4px;
    margin: 1rem 0;
  }
</style>

メタデータの動的生成

<script>
  import { page } from '$app/stores';
  
  // 動的なページタイトルとメタデータ
  $: pageTitle = getPageTitle($page);
  $: pageDescription = getPageDescription($page);
  
  // ページに基づいたタイトルの生成
  function getPageTitle(page) {
    const baseName = 'マイアプリ';
    
    if (page.route.id === '/') {
      return baseName;
    }
    
    if (page.route.id === '/blog/[slug]') {
      return `${page.data.post?.title || 'ブログ記事'} | ${baseName}`;
    }
    
    if (page.route.id === '/products/[id]') {
      return `${page.data.product?.name || '商品'} | ${baseName}`;
    }
    
    // デフォルト: パスの最初のセグメントを使用
    const segment = page.url.pathname.split('/')[1] || '';
    return `${segment.charAt(0).toUpperCase() + segment.slice(1)} | ${baseName}`;
  }
  
  function getPageDescription(page) {
    if (page.route.id === '/') {
      return 'マイアプリのホームページへようこそ';
    }
    
    if (page.route.id === '/blog/[slug]') {
      return page.data.post?.excerpt || 'ブログ記事の詳細';
    }
    
    if (page.route.id === '/products/[id]') {
      return page.data.product?.description || '商品の詳細情報';
    }
    
    return 'マイアプリのページです';
  }
</script>

<svelte:head>
  <title>{pageTitle}</title>
  <meta name="description" content={pageDescription} />
  <meta property="og:title" content={pageTitle} />
  <meta property="og:description" content={pageDescription} />
  <meta property="og:url" content={$page.url.href} />
</svelte:head>

<!-- ページコンテンツ -->
<slot />

新しい $app/state モジュールへの移行

SvelteKit 2.12 以降および Svelte 5 を使用している場合は、以下のように $app/state モジュールを使用するようにコードを更新することをお勧めします:

<script>
  // $app/stores からのインポートを置き換える
  // import { page } from '$app/stores';
  
  // 代わりに $app/state からインポート
  import { page } from '$app/state';
  
  // ルーンを使用したリアクティブな値
  const pathname = $derived(page.url.pathname);
  const params = $derived(page.params);
  const userData = $derived(page.data?.user);
  const hasError = $derived(page.error !== null);
</script>

<h1>現在のパス: {pathname}</h1>

{#if Object.keys(params).length > 0}
  <pre>{JSON.stringify(params, null, 2)}</pre>
{/if}

{#if userData}
  <div class="user">
    <h2>{userData.name}</h2>
  </div>
{/if}

{#if hasError}
  <div class="error">
    エラーが発生しました: {page.error.message}
  </div>
{/if}

page ストアは、SvelteKit アプリケーション内のほぼすべてのコンポーネントで現在のページ情報にアクセスするための基本的な方法です。これにより、動的なコンテンツ、条件付きレンダリング、ルートベースのスタイリングなど、さまざまな機能を実装できます。

updated

代わりに $app/state からの updated を使用してください(Svelte 5 が必要、詳細はドキュメントを参照)

初期値が false の読み取り専用ストアです。version.pollInterval が非ゼロ値の場合、SvelteKit はアプリケーションの新しいバージョンをポーリングで確認し、新バージョンを検出すると、ストアの値を true に更新します。updated.check() はポーリングに関係なく、即時チェックを強制します。

サーバー上では、このストアはコンポーネントの初期化中にのみ購読できます。ブラウザでは、いつでも購読することができます。

const updated: import('svelte/store').Readable<boolean> & {
	check(): Promise<boolean>;
};

解説

updated は SvelteKit の $app/stores モジュールが提供するストアで、以下のような特徴があります:

  1. アプリケーション更新の検出 – アプリケーションの新しいバージョンが利用可能になったかどうかを追跡します。
  2. ストアの値:
    • false – 初期値、または新しいバージョンが利用可能でない場合
    • true – 新しいバージョンが利用可能な場合
  3. メソッド:
    • check() – 新しいバージョンを即時にチェックするメソッド。新しいバージョンが利用可能な場合は true を解決する Promise を返します
  4. 設定との関連 – この機能は version.pollInterval が設定されている場合にのみ自動的に機能します。これは svelte.config.js で設定できます。
  5. 使用上の注意 – SvelteKit 2.12 以降では、$app/state モジュールからの updated の使用が推奨されています。これには Svelte 5 が必要です。

使用例

<script>
  import { updated } from '$app/stores';
  import { onMount } from 'svelte';
  import { fade } from 'svelte/transition';
  
  let showUpdateBanner = false;
  
  // 更新を定期的にチェック
  onMount(() => {
    // アプリ起動時に即時チェック
    updated.check().then(hasUpdate => {
      if (hasUpdate) {
        showUpdateBanner = true;
      }
    });
  });
  
  // 更新が利用可能になったときの処理
  $: if ($updated) {
    showUpdateBanner = true;
  }
  
  // 更新を適用(ページをリロード)
  function applyUpdate() {
    location.reload();
  }
  
  // バナーを閉じる
  function dismissBanner() {
    showUpdateBanner = false;
  }
</script>

<!-- 通常のアプリコンテンツ -->
<main>
  <slot />
</main>

<!-- 更新通知バナー -->
{#if showUpdateBanner}
  <div class="update-banner" transition:fade={{ duration: 300 }}>
    <div class="content">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 2v6h-6"></path>
        <path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
        <path d="M3 12a9 9 0 0 0 15 6.7L21 16"></path>
        <path d="M21 22v-6h-6"></path>
      </svg>
      <p>新しいバージョンが利用可能です</p>
    </div>
    <div class="actions">
      <button class="dismiss" on:click={dismissBanner}>
        後で
      </button>
      <button class="apply" on:click={applyUpdate}>
        今すぐ更新
      </button>
    </div>
  </div>
{/if}

<style>
  .update-banner {
    position: fixed;
    bottom: 20px;
    right: 20px;
    background: white;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    border-radius: 8px;
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    max-width: 320px;
    z-index: 1000;
  }
  
  .content {
    display: flex;
    align-items: center;
    gap: 12px;
  }
  
  .content svg {
    color: #4caf50;
    flex-shrink: 0;
  }
  
  .actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
  }
  
  button {
    padding: 8px 16px;
    border-radius: 4px;
    font-weight: 500;
    cursor: pointer;
    border: none;
  }
  
  .dismiss {
    background: transparent;
    color: #666;
  }
  
  .apply {
    background: #4caf50;
    color: white;
  }
  
  .dismiss:hover {
    background: #f0f0f0;
  }
  
  .apply:hover {
    background: #43a047;
  }
</style>

設定例(svelte.config.js)

// svelte.config.js
import adapter from '@sveltejs/adapter-auto';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter(),
    version: {
      // 1分(60,000ミリ秒)ごとに更新をチェック
      pollInterval: 60000
    }
  }
};

export default config;

手動でのアップデートチェック

<script>
  import { updated } from '$app/stores';
  
  let checkingForUpdates = false;
  let lastChecked = null;
  
  async function checkForUpdates() {
    checkingForUpdates = true;
    
    try {
      const hasUpdates = await updated.check();
      lastChecked = new Date().toLocaleTimeString();
      
      if (hasUpdates) {
        console.log('新しいバージョンが見つかりました');
      } else {
        console.log('アプリケーションは最新です');
      }
    } catch (error) {
      console.error('更新の確認中にエラーが発生しました:', error);
    } finally {
      checkingForUpdates = false;
    }
  }
</script>

<div class="update-checker">
  <button on:click={checkForUpdates} disabled={checkingForUpdates}>
    {#if checkingForUpdates}
      更新を確認中...
    {:else}
      更新を確認
    {/if}
  </button>
  
  {#if lastChecked}
    <p>最終確認: {lastChecked}</p>
  {/if}
  
  {#if $updated}
    <div class="update-available">
      <p>新しいバージョンが利用可能です</p>
      <button on:click={() => location.reload()}>
        更新する
      </button>
    </div>
  {/if}
</div>

新しい $app/state モジュールへの移行

SvelteKit 2.12 以降および Svelte 5 を使用している場合は、以下のように $app/state モジュールを使用するようにコードを更新することをお勧めします:

<script>
  // $app/stores からのインポートを置き換える
  // import { updated } from '$app/stores';
  
  // 代わりに $app/state からインポート
  import { updated } from '$app/state';
  
  // ルーンを使用したリアクティブな値
  const isUpdateAvailable = $derived(updated.current);
  
  // 更新のチェック
  async function checkForUpdates() {
    const hasUpdate = await updated.check();
    if (hasUpdate) {
      console.log('新しいバージョンが利用可能です');
    } else {
      console.log('アプリケーションは最新です');
    }
  }
</script>

{#if isUpdateAvailable}
  <div class="update-banner">
    <p>新しいバージョンが利用可能です</p>
    <button on:click={() => location.reload()}>更新する</button>
  </div>
{/if}

updated ストアは、アプリケーションの新しいバージョンが利用可能になったときにユーザーに通知し、シームレスな更新体験を提供するための強力なツールです。これにより、ユーザーが常に最新バージョンのアプリケーションを使用するよう促すことができます。

おわりに

今日は、 SvelteKitのリファレンスについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました