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 },
}
これで表示ができた
リクエストも飛んでいる
結局は MSW でレスポンスをモックする方法がそのまま使える。
supabase.auth.user()
はどうやってモックするんだろう。それはまた次回
参考