1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SvelteKitで作るフルスタック個人ブログ | 第3回: Svelte-QueryでAPIデータの取得と表示

Posted at

こんにちは!「SvelteKitで作るフルスタック個人ブログ」シリーズの第3回へようこそ。第2回では、SvelteKitのファイルベースのルーティングを活用し、ブログの記事一覧と詳細ページを静的データで構築しました。今回は、Svelte-Queryを使って外部APIから動的に記事データを取得し、ブログをリアルなアプリケーションに進化させます。React Queryとの比較も交えながら、SvelteKitとSvelte-Queryのスムーズな連携を体感しましょう!


この記事の目標

  • Svelte-Queryの基本を理解し、APIデータの取得・キャッシュ管理を行う。
  • 外部API(JSONPlaceholder)を活用して、ブログ記事を動的に表示する。
  • ローディング状態やエラーハンドリングを簡単に実装する。
  • Skeleton UIでユーザー体験(UX)を向上させる。
  • React Queryとの違いを比較し、Svelte-Queryの開発体験(DX)の良さを確認する。

最終的には、静的データに依存せず、外部APIから記事データを取得してブログを動的に表示できるようにします。では、始めましょう!


Svelte-Queryとは?

Svelte-Queryは、Svelte向けのデータフェッチングライブラリで、React Queryにインスパイアされています。APIリクエストを簡単に管理し、以下のような機能を提供します:

  • 自動キャッシュ:取得したデータをキャッシュして再利用。
  • ローディング/エラー状態:状態管理を簡潔に処理。
  • 自動リフェッチ:データが古くなった場合に自動更新。

React Queryを使ったことがある方は、Svelte-QueryのAPIが似ていると感じるでしょう。ただし、Svelteのリアクティブな特性を活かし、Svelteストアとの統合がスムーズです。Reactと比べて、ボイラープレートコードが少なく、直感的な記述が可能です。


Svelte-Queryのセットアップ

1. インストール

Svelte-Queryをプロジェクトに追加します。以下のコマンドを実行:

npm install @sveltestack/svelte-query

Svelte-Queryは、プロバイダーコンポーネントをセットアップする必要がある場合がありますが、今回は基本的なuseQueryフックを直接使います。

2. APIの準備

この記事では、外部APIとしてJSONPlaceholderを使用します。JSONPlaceholderは、ブログ記事のようなデータを取得できる無料のモックAPIです。以下のエンドポイントを利用:

  • https://jsonplaceholder.typicode.com/posts:記事一覧。
  • https://jsonplaceholder.typicode.com/posts/:id:記事詳細。

試しに、ブラウザやcurlhttps://jsonplaceholder.typicode.com/postsを開くと、以下のようなデータが返されます:

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere...",
    "body": "quia et suscipit..."
  },
  ...
]

このデータをブログ記事として扱います。


ブログ一覧ページの動的データ対応

1. 記事一覧の更新

第2回で作成したsrc/routes/blog/+page.svelteを、Svelte-Queryを使って外部APIからデータを取得するように修正します。以下のように編集:

<script>
  import { useQuery } from '@sveltestack/svelte-query';
  import { Card, CardHeader, CardContent, CardFooter, Spinner } from '@skeletonlabs/skeleton';

  const query = useQuery('posts', async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    if (!response.ok) throw new Error('データの取得に失敗しました');
    return response.json();
  });
</script>

<div class="container mx-auto p-8">
  <h1 class="h1 mb-8">ブログ記事一覧</h1>

  {#if $query.isLoading}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $query.isError}
    <p class="text-error-500">エラー: {$query.error.message}</p>
  {:else}
    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
      {#each $query.data as post}
        <Card>
          <CardHeader>
            <h2 class="h3">{post.title}</h2>
            <p class="text-sm text-surface-500">投稿ID: {post.id}</p>
          </CardHeader>
          <CardContent>
            <p>{post.body.slice(0, 100)}...</p>
          </CardContent>
          <CardFooter>
            <a href="/blog/{post.id}" class="btn variant-filled-primary">続きを読む</a>
          </CardFooter>
        </Card>
      {/each}
    </div>
  {/if}
</div>
  • 解説
    • useQueryフックを使って、postsキーでAPIデータを取得。
    • $query.isLoadingでローディング状態を表示(Skeleton UISpinnerを使用)。
    • $query.isErrorでエラー状態を処理。
    • データが取得できたら($query.data)、記事をカード形式で表示。
    • JSONPlaceholderのidslugとして詳細ページにリンク。

2. React Queryとの比較

React Queryで同じ機能を実装する場合、以下のようなコードになります:

// React
import { useQuery } from 'react-query';

function BlogList() {
  const { isLoading, isError, error, data } = useQuery('posts', async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts');
    if (!response.ok) throw new Error('Failed to fetch');
    return response.json();
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.message}</div>;

  return (
    <div className="container">
      {data.map(post => (
        <div key={post.id} className="card">
          <h2>{post.title}</h2>
          <p>{post.body.slice(0, 100)}...</p>
          <a href={`/blog/${post.id}`}>Read more</a>
        </div>
      ))}
    </div>
  );
}

React Queryも強力ですが、状態管理(isLoading, isError)をJSX内で条件分岐する必要があります。一方、Svelte-QueryはSvelteのリアクティブストア$query)を活用し、テンプレート内で条件(#if)を自然に記述できます。これにより、コードがHTMLに近い感覚で書け、読みやすさが向上します。


記事詳細ページの動的データ対応

1. 詳細ページの更新

src/routes/blog/[slug]/+page.svelteを以下のように修正:

<script>
  import { useQuery } from '@sveltestack/svelte-query';
  import { Spinner } from '@skeletonlabs/skeleton';
  import { error } from '@sveltejs/kit';

  export let data;

  const query = useQuery(['post', data.slug], async () => {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${data.slug}`);
    if (!response.ok) throw error(404, '記事が見つかりません');
    return response.json();
  });
</script>

<div class="container mx-auto p-8">
  {#if $query.isLoading}
    <div class="flex justify-center">
      <Spinner />
    </div>
  {:else if $query.isError}
    <p class="text-error-500">エラー: {$query.error.message}</p>
  {:else}
    <article class="card p-6 bg-surface-50">
      <header class="mb-4">
        <h1 class="h2">{$query.data.title}</h1>
        <p class="text-sm text-surface-500">投稿ID: {$query.data.id}</p>
      </header>
      <section class="prose">
        <p>{$query.data.body}</p>
      </section>
      <footer class="mt-6">
        <a href="/blog" class="btn variant-ghost-primary">一覧に戻る</a>
      </footer>
    </article>
  {/if}
</div>
  • 解説
    • useQueryの第一引数に['post', data.slug]を指定し、キャッシュキーを動的に設定。
    • data.slugは、第2回で作成した+page.jsから渡される(以下参照)。
    • ローディングやエラー状態を処理し、データが取得できたら記事を表示。
    • Skeleton UISpinnerでローディングを視覚的に表現。

2. ルートデータの確認

src/routes/blog/[slug]/+page.jsは変更不要ですが、念のため確認:

export function load({ params }) {
  return {
    slug: params.slug
  };
}

このload関数は、URLの[slug](例:/blog/11)をdata.slugとしてページに渡します。

3. React/Next.jsとの比較

Next.jsで同様の詳細ページを作る場合、動的データを取得するためにgetServerSidePropsgetStaticPropsを使います:

// Next.js
export async function getServerSideProps({ params }) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.slug}`);
  const post = await response.json();
  return { props: { post } };
}

Next.jsでは、サーバーサイドでのデータ取得が強力ですが、クライアントサイドのキャッシュ管理には別途ライブラリ(React Queryなど)が必要です。一方、Svelte-Queryはクライアントサイドのデータフェッチを簡潔に処理し、SvelteKitのload関数と自然に連携します。


動作確認

プロジェクトを起動します:

npm run dev

ブラウザで以下を確認:

  • http://localhost:5173/blog:JSONPlaceholderから取得した記事一覧が表示。ローディング中はSpinnerが表示され、エラーがあればエラーメッセージが出る。
  • http://localhost:5173/blog/1:記事詳細ページ。指定したIDの記事が表示され、同様にローディングやエラーを処理。

Svelte-Queryのおかげで、データフェッチングが簡単になり、Skeleton UIのコンポーネントがUXを向上させています。React Queryを使った場合に比べ、Svelte-QueryはSvelteのストアを活用することで、状態管理のコードが少なく、直感的な記述が可能です。


やってみよう!(チャレンジ)

記事一覧ページに「リフレッシュ」ボタンを追加して、データを手動で再取得してみましょう!Svelte-Queryのrefetch関数を使えば簡単に実装できます。以下は例:

<CardFooter>
  <a href="/blog/{post.id}" class="btn variant-filled-primary">続きを読む</a>
  <button on:click={() => $query.refetch()} class="btn variant-ghost">リフレッシュ</button>
</CardFooter>

また、Svelte-Queryのドキュメント(公式サイト)を参考に、staleTimecacheTimeを設定してキャッシュの動作をカスタマイズしてみてください。これで、データ管理の柔軟性をさらに体感できます!


まとめ

この記事では、Svelte-Queryを使って外部API(JSONPlaceholder)からブログ記事データを取得し、動的に表示しました。ローディングやエラーハンドリングを簡単に実装し、Skeleton UIでユーザー体験を向上させました。React QueryやNext.jsと比べ、Svelte-QueryとSvelteKitの組み合わせは、少ないコードで直感的なデータフェッチングを実現します。

次回は、SvelteKitのAPIルートとDrizzle ORMを使って、独自のバックエンドを構築し、ブログ記事をデータベースに保存・取得する方法を解説します。お楽しみに!

この記事が役に立ったと思ったら、LGTMストックしていただけると励みになります!質問や改善アイデアがあれば、コメントで教えてください。次の記事でまたお会いしましょう!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?