SvelteKitでSSR時のストアのハイドレーションを考えてみた

SvelteKit触ってみていいじゃん！と思ったんですが、よそのフレームワークにある**あの機能**はないのかな？と思い自分なりの解決方法が見えたので共有します。
この記事では「NuxtではSSR時にストア（Vuex）がクライアントでハイドレーションされるのにSvelteKitにはないの？」という疑問と、（私なりの）その解決方法を紹介します。

# SvelteKitはストアをハイドレーションするか？
__しません。__
もしかしたら将来的にそういう機能が実装されるかもしれませんが現時点（1.0.0-next.377）ではハイドレーションしません。

そもそもなぜストアをハイドレーションしたいのかというと、以下のような理由があげられます。

1. SSRとCSRで二重でHTTP API等を呼び出したくない
1. SSRで取得した値を再利用したい
1. ストアから値を取り出すだけの使い方をしたい

この要望をSvelteKitの仕組みでうまく隠蔽？したいと思います。

# SvelteKitでストアをハイドレーションさせる
機能はなくてもやりたいことは実装できます。
厳密にはストアそのものをハイドレーションするわけではないですが、次のようなフローでサーバー側で取得した値をクライアントでストアに詰め直します。


```mermaid
sequenceDiagram
  participant B as ブラウザ
  participant S as サーバー
  participant ST as ストア
  participant A as APIサーバー
  B->>+S: リクエスト
  S->>+A: (1) APIリクエスト(Endpoints)
  A->>S: 
  S-->>ST: ストアに保存
  S->>B: SSR
  S-->>B: (2) Load props(SvelteKit)
  B-->>ST: (3) クライアントでストアに保存 
```

順を追って説明します。

## 1.SvelteKitのエンドポイントを利用してAPIリソースを取得する
SvelteKitのエンドポイントはSSR時はサーバーサイドで取得され、そのレスポンスはハイドレーションされてページコンポーネントのpropsで取得できるようになります。
CSRの場合は遷移する前に同ページパスに加えて__data.jsonでリクエストされてその取得した値が遷移先のページコンポーネントのpropsで利用できるようになります。

つまり、ストアに詰めた値はハイドレーションされませんが、エンドポイントのレスポンスの値はハイドレーションされることが重要です。

※以下の例ではAPIのレスポンスに型定義をつけるために[aspida](https://github.com/aspida/aspida)を利用しています。

```ts:src/routes/weather.ts
import type { RequestHandler, RequestHandlerOutput } from '@sveltejs/kit'
import aspida from '@aspida/node-fetch'
import fetch from 'node-fetch'
import api from '$api/$api'
import type { Weather } from '$api/data/2.5/weather'

const fetchConfig = {
  credentials: "include",
  baseURL: "https://api.openweathermap.org",
  throwHttpErrors: true // throw an error on 4xx/5xx, default is false
}

export type GetOutput = { data: Weather }

export const GET: RequestHandler = async (): Promise<RequestHandlerOutput<GetOutput>> => {
  const client = api(aspida(fetch, fetchConfig))
  const data = await client.data.$2_5.weather.$get({
    query: {
      lat: 35.68944,
      lon: 139.69167,
      appid: '***********'
    }
  })
  return {
	status: 200,
	headers: {},
	body: {
      data
    }
  }
}
```

aspidaを利用しているためGETの返す値の型定義を`RequestHandlerOutput`のジェネリクスに指定することができます。

## 2.Load関数でエンドポイントのレスポンスをPropsで取得する
SvelteKitのエンドポイントのレスポンスはLoad関数の`props`で受け取ることができます。

```ts:src/routes/weather.svelte
<script context="module" lang="ts">
  import type { Load } from '@sveltejs/kit'
  import type { GetOutput } from '.'

  export const load: Load<{}, GetOutput> = ({ props }) => {
    return {}
  }

	export const hydrate = true
</script>
```
Loadの型定義で第二引数に指定した型がPropsの型となります。
エンドポイントで利用している`GetOutput`型を指定することで型の恩恵を受けることができます。
![スクリーンショット 2022-07-28 20.52.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89213/e75cf7d7-d5da-21b0-db91-ea759cd6c3e5.png)
`props`の中にdataプロパティがある。その中にはAPIのJSONが入っていることがわかる。
![スクリーンショット 2022-07-28 20.53.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89213/dcede1a4-949c-7c2f-64d5-f98946611d5e.png)
（ちゃんと型補完されてよし！）

## 3.ストアにデータを保持させる

ストアの実装は次のようになっています。Svelteは独自でストア機能を持っているのでそれを利用しています。
`setter`関数でAPIのレスポンスをストアにセットするようになっています。

```ts:src/lib/stores/weatherStore.ts
import { writable } from 'svelte/store'
import type { Weather } from '$api/data/2.5/weather'

type WeatherStore = {
  name: string
  coord: {
    lon: number
    lat: number
  },
  weather: { id: number, main: string, description: string, icon: string }[]
}

const store = writable<WeatherStore>({
  name: '',
  coord: {
    lon: 0,
    lat: 0,
  },
  weather: [{
    id: 0,
    main: '',
    description: '',
    icon: '',
  }]
})

const setter = (apiRes: Weather) => {
  store.set(apiRes)
}

export const useWeatherStore = () => {
  return {
    store,
    setter
  }
}
```

このストアを先程のLoad関数で呼び出します。

```ts:src/routes/weather.svelte
import { useWeatherStore } from '$lib/stores/weatherStore'

export const load: Load<{}, GetOutput> = ({ props }) => {
  useWeatherStore().setter(props.data)
  return {}
}
```

あとはお好きなコンポーネントでストアを呼び出してその値にアクセスするだけです。

```ts:src/lib/components/weather.svelte
<script lang="ts">
  import { useWeatherStore } from '$lib/stores/weatherStore'
  const weather = useWeatherStore().store
</script>

<p>{ $weather.name }</p>
<ul>
  {#each $weather.weather as item}
  <li>{ item.description }</li>
  {/each}
</ul>
```

Svelteではストアの値にアクセスする際に接頭辞に`$`をつけるだけです。
![スクリーンショット 2022-07-28 21.02.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89213/e28005df-9ca0-d0e3-9a10-5b3d4f27e19c.png)
ちゃんと型による恩恵を受けていることがわかります。

![スクリーンショット 2022-07-28 21.02.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89213/b5385c27-b771-a729-cca8-252c5a70e2ff.png)

SvelteKitはストアの値をハイドレーションすることができませんがエンドポイントの仕組みを利用することで、ストアがハイドレーションされたかのように扱うことができました。
最初にあげたストアをハイドレーションしたいという理由を十分に満たすことができたと思います。

それでは！

## おまけ

- この内容は[Svelte交流会#1](https://speakerdeck.com/kubotak/sveltekittesutoafalsehaitoresiyonwokao-etemita)で発表した内容です。
- SvelteKitのStoreはリクエスト毎に独立しているわけではないので注意が必要です（別途記事で対処法出せたら・・・）
- ページエンドポイントやLoad関数周りの仕様が変更になる可能性があります。[参照](https://github.com/sveltejs/kit/discussions/5748)