🐡
Storybook tips: Contextを使っているコンポーネントのStoryを書く
StorybookのTipsです。
内部でuseContext
を使っているコンポーネントをどうやってStoryに起こすかを解説します。
コンポーネントの例
以下のように、内部でAuthContextを使っているコンポーネントがあったとします。
export const Header = memo(() => {
const { user } = useContext(AuthContext)
return (
<header>
<ServiceLogo></ServiceLogo>
{
user && user.type === 'teacher' && <Text>{user.name}</Text>
}
{
user && <LogOutButton></LogOutButton>
}
</header>
)
})
これをそのままComponent Story Formatに落とし込むと、ContextでWrapされていないため動作しません。
export default {
component: Header
} as ComponentMeta<typeof Header>
// 以下のそれぞれのストーリーで、Providerから返ってくるuserのオブジェクトが異なる
export const ByTeacher: ComponentStoryObj<typeof Header> = {
}
export const ByLogoutUser: ComponentStoryObj<typeof Header> = {
}
export const ByStudent: ComponentStoryObj<typeof Header> = {
}
global decoratorの例
Storybookの機能として、preview.js(tsx)
にてProviderでラップする処理をdecorators
に指定する方法がUIフレームワーク利用の際に主に使われますが、今回はこのストーリーにおいてのみProviderを適用したいので、向いていないです。
const withChakra = (StoryFn: Function) => {
return (
<ChakraProvider theme={theme}>
<StoryFn />
</ChakraProvider>
);
};
export const decorators = [withChakra]; // ここに書き足すと、全ストーリーに同じProviderが強制的に適用される
対応方法
実はストーリー単位でdecorators
を設定できるため、以下のようにStoryをProvider
で囲うことで対応できます。
export const ByTeacher: ComponentStoryObj<typeof Header> = {
decorators: [
(Story) => {
return (
<AuthContext.Provider
value={{
user: {
type: 'teacher',
name: 'taro'
},
dispatch: () => undefined as any,
}}
>
<Story />
</AuthContext.Provider>
);
},
],
};
この方法だとストーリー単位で別々の値をProviderから流し込むことができるので、コンポーネント内部の出し分けを容易に再現可能です。
Pros/Cons
このように各ストーリーでdecorators
に自前のContextを指定するメリットは、コンポーネント設計の粒度が大きくて、Contextと密結合していることが多くても問題なくStorybookで再現できることです。
デメリットとしては、そもそもコンポーネントをContextを受け取るContainer役のコンポーネントと、純粋にPropsから受け取ったデータを表示するPresentation役のコンポーネントに切り分けていれば本記事の工夫は不要のため、雑なコンポーネント設計を助長する可能性があります。
Reference
Discussion