🤖

useSWRを使ったReactコンポーネントをStorybookで正しく表示する方法

に公開

概要

こんにちは、駆け出しWebエンジニアのふっけーです。今回はuseSWRを使ったReactコンポーネントをStorybookに正しく表示する方法を紹介します。
useSWRを使ったコンポーネントを正しく表示させる際、useSWRで呼び出されているAPIをMockする必要があります。Mockのやり方としては2つあります:

MockServiceWorkerに関する記事は多くあったので、今回はMiddlewareを使ったやり方について紹介したいと思います。

対象読者

  • NextJS+useSWR+Hono+Storybookの構成でUIを確認したい人
  • useSWRの初心者〜中級者
  • Storybookが何となくわかる (基本的な設定・decoratorsがわかる)

サンプルコンポーネント

このコンポーネントは渡されたidをもとにAPIからデータを取得し、そのデータを表示するシンプルな構成です。

UIコンポーネント (ShowAPIData.tsx)

import { useAPIData } from "@/hooks/useAPIData"

type Props = {
  id: string
}

export default function ShowAPIData({id}: Props) {
  const { data, isLoading, isError } = useAPIData(id)

    return (
    <>
      {!isLoading && !isError && data && (
        <p>{data}</p>
      )}
    </>
    )
}

Hono Endpoints

import { Hono } from "hono"

export const app = new Hono().basePath("/api")

export const route = app
  .get("/",zValidator("query",z.object({ id: z.string() })),async (c) => {
    return c.json({
      data: "APIResponse", //今回はこちらはこのようにして割愛させていただきます
    })
  })

useAPIData Hooks

import useSWR from "swr"
import { hc, InferRequestType } from "hono/client"
import type { AppType } from "@/be"

export const useAPIData = (id: string) => {
  const client = hc<AppType>("/")
  const $get = client.api.$get

  const fetcher = async (args: InferRequestType<typeof $get>) => {
    const res = await $get(args)
    if (!res.ok) {
      return undefined
    }
    const { data } = await res.json()
    return data
  }

  const { data, error, isLoading } = useSWR(
    id
      ? {
          query: {
            id: id,
          },
        }
      : null,
    fetcher,
  )
  return {
    data,
    isLoading,
    isError: error,
  }
}

UIコンポーネントに対応するStory

import ShowAPIData from "./ShowAPIData"

const meta = {
  title: "ShowAPIData",
  component: ShowAPIData,
  argTypes: {
    id: {
      control: "text",
      description: "",
    },
  },
  decorators: [
      (Story) => (
        <SWRConfig value={{ use: [testMiddleWare] }}>
          <Story />
        </SWRConfig>
      ),
  ],
} satisfies Meta<typeof ShowAPIData>

export default meta

export const Default: Story = {
  args: {
    id: "1234"
  }
}

Middlewareを適用していきましょう

このままだと、argsをどんなに変えてもなにも表示されません。そこで、useSWRのMiddlewareを導入していきましょう
Middlewareはテストだけでなく、loggerを実行したい場合などにも使えます。公式Docsにはこのような記載がありました:

ミドルウェアは SWR フックを受け取り、実行の前後にロジックを実行できます。複数のミドルウェアがある場合、各ミドルウェアは次のミドルウェアをラップします。リストの最後のミドルウェアは、元の SWR フックである useSWR を受け取ります。

function myMiddleware (useSWRNext) {
 return (key, fetcher, config) => {
   // フックが実行される前...

   // 次のミドルウェア、またはこれが最後のミドルウェアの場合は `useSWR` を処理します。
   const swr = useSWRNext(key, fetcher, config)

   // フックが実行された後...
   return swr
 }
}

オプションとして、ミドルウェアの配列を SWRConfig または useSWR に渡すことができます:

<SWRConfig value={{ use: [myMiddleware] }}>
// または...
useSWR(key, fetcher, { use: [myMiddleware] })

JSXを使ってSWRConfigをStorybookに注入できるため、Mockの適用が柔軟かつ簡潔に行えるのが特徴です。
これらを踏まえてMiddlewareを追加していきましょう

import ShowAPIData from "./ShowAPIData"

type MockData = {
  key: string
  data: string
}

const mockData: MockData[] = [
  {
    key: "empty_data",
    data: "",
  },
  {
    key: "very_short_data",
    data: "a",
  },
  {
    key: "normal_data",
    data: "abcdefg",
  },
  {
    key: "very_long_data",
    data: "abcdefghijklmnopqrstuvwxyz",
  }
]

const testMiddleWare: Middleware = () => {
  return (key): SWRResponse => {
    // SWRキーからidを抽出
    let id: string = "normal_data" // デフォルト値

    if (key && typeof key === "object" && "query" in key) {
      const query = key.query as { id?: string }
      id = query.id || ""
    }

    const mockEntry = mockData.find((mock) => mock.key === id)?.data

    return {
      data: mockEntry,
      error: undefined,
      mutate: () => Promise.resolve(),
      isValidating: false,
      isLoading: false,
    }
  }
}

const meta = {
  title: "ShowAPIData",
  component: ShowAPIData,
  argTypes: {
    id: {
      control: "text",
      description: "",
    },
  }
} satisfies Meta<typeof ShowAPIData>

export default meta

export const Default: Story = {
  args: {
    id: "1234"
  }
}

ここのMiddlewareで行なっていることは、SWRのレスポンスであるSWRResponseをMockしてreturnしているところです。
Docsにも書いてある通り、リストの最後のミドルウェアは、元の SWR フックである useSWR を受け取ります。のため、実際にAPIを叩くことなくMockのResponseを返すことができるのです。
また、少しわかりにくいですがHonoのClientを使っているためidを取得するときに下記のような書き方をしています:

let id: string = "normal_data" // デフォルト値

if (key && typeof key === "object" && "query" in key) {
  const query = key.query as { id?: string }
  id = query.id || ""
}

これは、keyがanyになってしまうためtypeofで型チェックを挟むことでSWRのqueryから取得できるようにしています

まとめ

  • useSWRのMiddlewareを用いるとMock Service Workerを使うことなくAPIをMockすることができる
  • Middlewareを使ったときにswrのqueryから引数を持ってくるときには少し工夫が必要だった

最後まで読んでいただきありがとうございました!
記事が参考になりましたら❤️お願いします!

Discussion