SvelteKit触ってみていいじゃん!と思ったんですが、よそのフレームワークにあるあの機能はないのかな?と思い自分なりの解決方法が見えたので共有します。
この記事では「NuxtではSSR時にストア(Vuex)がクライアントでハイドレーションされるのにSvelteKitにはないの?」という疑問と、(私なりの)その解決方法を紹介します。
SvelteKitはストアをハイドレーションするか?
しません。
もしかしたら将来的にそういう機能が実装されるかもしれませんが現時点(1.0.0-next.377)ではハイドレーションしません。
そもそもなぜストアをハイドレーションしたいのかというと、以下のような理由があげられます。
- SSRとCSRで二重でHTTP API等を呼び出したくない
- SSRで取得した値を再利用したい
- ストアから値を取り出すだけの使い方をしたい
この要望をSvelteKitの仕組みでうまく隠蔽?したいと思います。
SvelteKitでストアをハイドレーションさせる
機能はなくてもやりたいことは実装できます。
厳密にはストアそのものをハイドレーションするわけではないですが、次のようなフローでサーバー側で取得した値をクライアントでストアに詰め直します。
順を追って説明します。
1.SvelteKitのエンドポイントを利用してAPIリソースを取得する
SvelteKitのエンドポイントはSSR時はサーバーサイドで取得され、そのレスポンスはハイドレーションされてページコンポーネントのpropsで取得できるようになります。
CSRの場合は遷移する前に同ページパスに加えて__data.jsonでリクエストされてその取得した値が遷移先のページコンポーネントのpropsで利用できるようになります。
つまり、ストアに詰めた値はハイドレーションされませんが、エンドポイントのレスポンスの値はハイドレーションされることが重要です。
※以下の例ではAPIのレスポンスに型定義をつけるためにaspidaを利用しています。
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
で受け取ることができます。
<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
型を指定することで型の恩恵を受けることができます。
props
の中にdataプロパティがある。その中にはAPIのJSONが入っていることがわかる。
(ちゃんと型補完されてよし!)
3.ストアにデータを保持させる
ストアの実装は次のようになっています。Svelteは独自でストア機能を持っているのでそれを利用しています。
setter
関数でAPIのレスポンスをストアにセットするようになっています。
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関数で呼び出します。
import { useWeatherStore } from '$lib/stores/weatherStore'
export const load: Load<{}, GetOutput> = ({ props }) => {
useWeatherStore().setter(props.data)
return {}
}
あとはお好きなコンポーネントでストアを呼び出してその値にアクセスするだけです。
<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ではストアの値にアクセスする際に接頭辞に$
をつけるだけです。
ちゃんと型による恩恵を受けていることがわかります。
SvelteKitはストアの値をハイドレーションすることができませんがエンドポイントの仕組みを利用することで、ストアがハイドレーションされたかのように扱うことができました。
最初にあげたストアをハイドレーションしたいという理由を十分に満たすことができたと思います。
それでは!
おまけ
- この内容はSvelte交流会#1で発表した内容です。
- SvelteKitのStoreはリクエスト毎に独立しているわけではないので注意が必要です(別途記事で対処法出せたら・・・)
- ページエンドポイントやLoad関数周りの仕様が変更になる可能性があります。参照