SvelteKitでSSR時のストアを分離する

:::note warn
2022/11/13
`@sveltejs/kit@1.0.0-next.406`によるファイルベースのルーティングの仕様変更に伴い記事の内容を更新
:::

SvelteKitでは現時点のバージョン(`@sveltejs/kit@1.0.0-next.538`)においてストアをハイドレーションすることができません。
また、SSR時にサーバーで生成するストアはリクエスト単位ではなく、レスポンス後も状態が残ります。
そして他のリクエスト時に参照される・・・ということも起こりえます。

そこでこの記事ではSvelteKitでサーバー側でもリクエスト単位でストアを使えるようにする方法を紹介したいと思います。

# ストアを共存させないwritableを生成する
次の関数を用意します。

```ts
import { page } from '$app/stores'
import { v4 } from 'uuid'
import { get, writable as svelteWritable, type Writable } from 'svelte/store'

const storesKey = 'isolateSSRStore'

/**
 * @see https://github.com/sveltejs/kit/discussions/4339#discussioncomment-3258927
 * @param initialValue
 * @returns
 */
export const writable = <T>(initialValue: T): Writable<T> => {
  // NOTE: 不用意にinitialValueを書き変えられてしまうとバグの温床になるので防ぐ
  if (typeof initialValue === 'object') {
    initialValue = Object.freeze(initialValue)
  }

  const key = `store_${v4()}`
  const setStore = <T>(value: T) => {
    try {
      const { data } = get(page)

      const store = data[storesKey]?.[key]
      const currentValue = store ? store.value : initialValue

      if (!store || value !== currentValue) {
        if (!data[storesKey]) data[storesKey] = {}
        data[storesKey][key] = {
          value,
          subscribers: store?.subscribers || []
        }

        if (store) {
          store.subscribers.forEach((fn: CallableFunction) => {
            fn(value)
          })

          return value
        }
      }
    } catch (error) {
      console.error(error)
      return value
    }
  }

  const sandboxedWritable: Writable<T> = {
    set: setStore,
    subscribe: (fn: CallableFunction) => {
      try {
        const { data } = get(page)

        const store = data[storesKey]?.[key]
        const currentValue = store ? store.value : initialValue

        fn(currentValue)

        if (!data[storesKey]) data[storesKey] = {}
        data[storesKey][key] = {
          value: store?.value || initialValue,
          subscribers: [...(store?.subscribers || []), fn]
        }
      } catch (error) {
        console.error(error)
        fn(initialValue)
      }

      return function unsubscribe() {
        try {
          const { data } = get(page)
          const { subscribers } = data[storesKey][key]
          subscribers.splice(subscribers.indexOf(fn), 1)
        } catch (error) {
          console.error(error)
        }
      }
    },
    update: (fn: CallableFunction) => {
      try {
        const { data } = get(page)

        const store = data[storesKey]?.[key]
        const currentValue = store ? store?.value : initialValue

        setStore(fn(currentValue))
      } catch (error) {
        console.error(error)
        setStore(fn(initialValue))
      }
    }
  }

  return typeof window !== 'undefined' ? svelteWritable<T>(initialValue) : sandboxedWritable
}

```

Svelte標準のwritableではなく、拡張したこのwritableからストアを定義してください。

　　終
制作・著作
━━━━━
　Ⓝ●Ⓚ

## 少し解説
この実装でなぜストアがリクエスト単位になるかというと、
`$app/stores`で取得したpageの`data`を使っていることがポイントとなります。
`data`はリクエストされた際にレイアウトサーバーやページサーバーから返却されたページ用のデータを格納した変数です。
この`data`はリクエスト後に破棄されるので、その`data`の中にストアを登録してリクエスト毎に管理します。
また、UUIDをキーとすることでそのリクエストで使うストアを独立させています。

https://kit.svelte.dev/docs/load#page-data

# 最後に
ちなみのこのコードはSvelteKitのDiscussionsで紹介されているコードを`@1.0.0-next.406`以降の仕様に対応させ、TypeScriptに置き換えたものになります。

https://github.com/sveltejs/kit/discussions/4339#discussioncomment-3258927

この仕組自体がSvelteKitに組み込まれていると嬉しいですね・・・！

おわり