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

こんにちは！「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をプロジェクトに追加します。以下のコマンドを実行：

```bash
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`：記事詳細。

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

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

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

---

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

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

```svelte
<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 UI**の`Spinner`を使用）。
  - `$query.isError`でエラー状態を処理。
  - データが取得できたら（`$query.data`）、記事をカード形式で表示。
  - JSONPlaceholderの`id`を`slug`として詳細ページにリンク。

### 2. React Queryとの比較
React Queryで同じ機能を実装する場合、以下のようなコードになります：

```jsx
// 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`を以下のように修正：

```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 UI**の`Spinner`でローディングを視覚的に表現。

### 2. ルートデータの確認
`src/routes/blog/[slug]/+page.js`は変更不要ですが、念のため確認：

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

この`load`関数は、URLの`[slug]`（例：`/blog/1`の`1`）を`data.slug`としてページに渡します。

### 3. React/Next.jsとの比較
Next.jsで同様の詳細ページを作る場合、動的データを取得するために`getServerSideProps`や`getStaticProps`を使います：

```jsx
// 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`関数と自然に連携します。

---

## 動作確認
プロジェクトを起動します：

```bash
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`関数を使えば簡単に実装できます。以下は例：

```svelte
<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のドキュメント（[公式サイト](https://svelte-query.vercel.app/)）を参考に、`staleTime`や`cacheTime`を設定してキャッシュの動作をカスタマイズしてみてください。これで、データ管理の柔軟性をさらに体感できます！

---

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

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

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