Open5

Supabase を使ってデータ取得しているコンポーネントを Storybook で表示する

プログラミングをするパンダプログラミングをするパンダ

Supabase をモックして、Supabase を使ってるコンポーネントを Storybook で表示したい。

ライブラリのバージョン

next: v11.1.2
react: v17.0.2
react-dom: v17.0.2

@supabase/supabase-js: v1.23.0
swr: v1.0.1

@storybook/react: v6.4.0-beta.1
msw: v0.35.0
msw-storybook-addon: v1.3.0

例えば以下のように DB の posts テーブルから post をidの降順で全て取得する処理を考える

export type PostSchema = {
  id: number
  title: string
  image: string | null
  userId: string
  createdAt: string
  updatedAt: string
}

export const findAllPost = async (): Promise<PostSchema[]> => {
  const { data, error } = await supabase.from<PostSchema>('posts').select().order('id', { ascending: true })

  if (error) {
    throw new Error(`error: ${error.message}`)
  }

  return camelcaseKeys(data)
}

supabase は DB へ接続しているわけではなく、APIコールを通じてデータ取得しているため、HTTPレスポンスをモックすればこれを実現できる。

上記の関数だと以下のようなURLにリクエストが飛んでいる。

http://subdomain.supabase.com/rest/v1/posts?select=*&order=id.asc.nullslast
プログラミングをするパンダプログラミングをするパンダ

上記の関数を呼び出す Posts コンポーネントは以下の通り。API リクエストなので、SWR を使っている。

import { findAllPost, PostSchema } from '@/lib/supabase'
import useSWR from 'swr'

type PostsProps = unknown

const Posts: React.VFC<PostsProps> = () => {
  const { data: posts } = useSWR<PostSchema[]>('db:posts.all', findAllPost)

  if (!posts) {
    return <div>loading</div>
  }

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          {post.id}: {post.title}
        </li>
      ))}
    </ul>
  )
}

export default Posts

これを Storybook で表示するためには、MSW を使う

プログラミングをするパンダプログラミングをするパンダ

公式ドキュメントに従いつつ、MSW の初期設定をする。storybook用の addon を使う。ここでは手順は省略。

次に投稿データを作る

// mocks/msw/fixtures/posts.ts
import { PostSchema } from '@/lib/supabase'
import camelcaseKeys from 'camelcase-keys'

export const posts: PostSchema[] = camelcaseKeys([
  {
    id: 19,
    title: 'finish',
    image: null,
    created_at: '2021-09-27T16:07:01.828396+00:00',
    updated_at: '2021-09-27T16:07:01.828396+00:00',
    user_id: 'dummy-main',
  },
  {
    id: 32,
    title: 'I am 32',
    image: null,
    created_at: '2021-09-27T16:39:44.776832+00:00',
    updated_at: '2021-09-27T16:39:44.776832+00:00',
    user_id: 'dummy-1',
  },
  {
    id: 1,
    title: 'you are the one',
    image: null,
    created_at: '2021-09-27T16:39:44.776832+00:00',
    updated_at: '2021-09-27T16:39:44.776832+00:00',
    user_id: 'dummy-1',
  },
  {
    id: 22,
    title: 'I can change',
    image: null,
    created_at: '2021-09-27T16:39:44.776832+00:00',
    updated_at: '2021-09-27T16:39:44.776832+00:00',
    user_id: 'dummy-1',
  },
])

ハンドラーでクエリに応じてデータ操作した posts を返す

// mocks/msw/handlers/posts.ts
import { rest } from 'msw'

import { posts } from '../fixtures/posts'

export const handlers = [
  rest.get('/rest/v1/posts', (req, res, ctx) => {
    const select = req.url.searchParams.get('select')
    const order = req.url.searchParams.get('order')

    if (select === '*') {
      if (order?.includes('asc')) {
        return res(ctx.json(posts.sort((prev, next) => (prev.id > next.id ? 1 : -1))))
      } else if (order?.includes('desc')) {
        return res(ctx.json(posts.sort((prev, next) => (prev.id > next.id ? -1 : 1))))
      }

      return res(ctx.json([posts]))
    }

    return res(ctx.status(404))
  }),
]

Supabase の初期化方法は以下。

export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

このとき、環境変数は .envから読み込まれる。このため、Storybook から本番環境に接続してしまいかねない。

このため、以下のように.env.developmentを作成し、Storybook と同じホストにリクエストを送るようにする。

Supabase のホスト名を Storybook と同じホスト名にしないと、Postsコンポーネントからのリクエストが CORS で弾かれる。

// .env.development
NEXT_PUBLIC_SUPABASE_URL=http://localhost:6006
NEXT_PUBLIC_SUPABASE_ANON_KEY=dummy-key

今回は Next.js を使っているのでNEXT_PUBLIC_の接頭辞をつけているが、これを外すと.envから環境変数を読み取ってしまう。あれ、なんで?

プログラミングをするパンダプログラミングをするパンダ

あとは storybook 用のコンポーネントを作成するだけ。 CSF3.0の形式で記述している

import { handlers } from '@/mocks/msw/handlers/posts'
import { ComponentStoryObj } from '@storybook/react'

import Posts from './Posts'

type Story = ComponentStoryObj<typeof Posts>

export default { component: Posts }

export const Guest: Story = {
  parameters: { msw: handlers },
}

これで表示ができた

Storybook で Posts コンポーネントが表示されている

リクエストも飛んでいる

chrome devtools でHTTPリクエストの情報を開いている