ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

外部ライブラリの提供するReact Contextの値をStorybookでコントロールする

宮崎からメリークリスマス🎅🌴 yoshinoです。
この記事は ESM Advent Calendar 2022 - Adventar 24日目の記事です。

私が参加しているプロジェクトではフロントエンドでNext.jsを利用していて、React.ComponentのUI管理にStorybookを利用しています。

Storybookはとても便利な反面、少しこった書き方をしようとすると、どうやって書けば良いのだっけ?となることがありました。 そこで、今回は、外部ライブラリのReact Contextを使うReact.Componentのstoryをどのように書くことができるかを考えてみようと思います。

対象となるコンポーネント

Auth0のSDKであるライブラリ(auth0-react)が提供するuseAuth0()というAuth0Context.Provider のcontextから値を取り出すメソッドを使っているコンポーネントを例として考えます。イメージはこんな感じです。

import { useAuth0 } from "@auth0/auth0-react"

function Auth0Menu() {
  const { isAuthenticated } = useAuth0()
  // isAuthenticated は型がboolean
  return <div>{isAuthenticated ?  <p>ログイン済み</p> : <p>未ログイン</p>}</div>
}

export default Auth0Menu

この記事では、StorybookのControlタブでisAuthenticatedの値を変更する方法を考えます。

うまくいったストーリー

結論としては、decoratorsのcontext経由でargsをわたすことで、想定した動作が実現できました 🌴

import React, { useState } from "react"
import { ComponentMeta, Story } from "@storybook/react"

import Auth0Menu from "../../components/Auth0Menu"
import { Auth0Context, Auth0ContextInterface } from "@auth0/auth0-react"

export default {
  title: "Auth0Menu",
  component: Auth0Menu,
  decorators: [
    (Story, context) => {
      return (
        {/* contextからはargsをはじめ様々な値を取り出すことができる */}
        <Auth0Context.Provider value={{isAuthenticated: context.args.isAuthenticated} as Auth0ContextInterface}>
          <Story />
        </Auth0Context.Provider>
      )
    }
  ]
} as ComponentMeta<typeof Auth0Menu>

const Template: Story<typeof Auth0Menu & {isAuthenticated: boolean}>  = ({isAuthenticated, ...args}) => <Auth0Menu {...args} />
export const Default = Template.bind({})
// argsで定義した値はStoryBookのcotnrolsタブで変更することができる
Default.args = {
  isAuthenticated: true
}

以上になります 🙆

思った通り動かなかったストーリー

番外編です。最初はMocking importsを利用してやることも検討していました。しかし、この方法では、Controlタブを使って動的に値を変更することはできませんでした。 parametersを使うので、異なるStory(異なるファイルで定義されているStory)であれば、違う値に設定することはできます。Storybookの理解の助けになったので、こちらも説明します。

__mocks__/auht0-react.ts で、対象のメソッド(useAuth0)をモックします。

import { StoryContext } from "@storybook/react"
import { PartialStoryFn } from '@storybook/csf'

let isAuthenticated: boolean;

export function useAuth0() {
  return { isAuthenticated: isAuthenticated }
}

// Storybookが提供するdecorator()メソッドを使ってuseAuth0の返り値にparametersの値を使う
// context.argsとして、argsにもアクセスできるが、Controlタブを使って動的に値を変更できない
export function decorator(story: PartialStoryFn, context: StoryContext) {
  if (context.parameters && context.parameters.auth0) {
    isAuthenticated = context.parameters.auth0.isAuthenticated
  }

  return story()
}

.storybook/main.js でimport時に、うえで作成したファイルを読み込むように変更します。

webpackFinal: async (config) => {
  config.resolve.alias['@auth0/auth0-react'] = require.resolve('../stories/__mocks__/auth0-react.ts');
  return config;
},

.storybook/preview.js でdecoratorの設定を読み込むようにします。

import { decorator } from '../stories/__mocks__/auth0-react';

export const decorators = [decorator];

あとはお好きなStoryで、こんな感じでparametersを設定してあげることで値のだし分けをすることができます。

export const Default = Template.bind({})
Default.parameters = {
  auth0: {
    isAuthenticated: true // or false
  }
}

Controlタブを使って動的に値を変更する必要がないケースでは、この方法でも良いかと思います 🙆

おわりに

紹介した例は具体的なライブラリを使ったものでしたが、今回の全体的な流れは、Contextの値をcontrolタブを使って、だし分けしたい場合には、汎用的に使える方法かと思います。

それでは、良いクリスマスをー 🏄