📖

StorybookでEmotionのThemeをProvideする

2022/07/18に公開

Next.jsとEmotionの構成で開発しているプロジェクトにてStorybookを導入しようということになったので、いざやってみようとしてみたところTheme周りでエラーが出たのでまとめてみました。

構成

react -> 18.2.0
next -> 12.1.6
emotion/react -> 11.9.3
storybook/react -> 6.5.9

例えばこんなコンポーネントがあったとして...

AppText.tsx
import { FC, ReactNode } from 'react'
import { css, Theme } from '@emotion/react'

import { Typography } from '@mui/material'

type Props = {
  children?: ReactNode
}

const AppText: FC<Props> = ({ children }) => {
  return <Typography css={textStyle}>{children}</Typography>
}

const textStyle = (theme: Theme) => css`
  color: ${theme.palette.text.secondary};
`

export default AppText

AppText自体に特に意味はないですが、ここで重要なのはtextStyleの中の

color: ${theme.palette.text.secondary};

のthemeを使っている部分です。

で、このthemeをProvideしているのが_app.tsx

_app.tsx
import '../styles/globals.css'
import { ReactElement, ReactNode } from 'react'
import type { AppProps } from 'next/app'
import { ThemeProvider } from '@emotion/react'
import { ThemeProvider as MUThemeProvider } from '@mui/material/styles'

import theme from '../theme'

import { NextPage } from 'next'

type NextPageWithLayout = NextPage & {
  getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  return (
      <MUThemeProvider theme={theme}>
        <ThemeProvider theme={theme}>
          <Component {...pageProps} />
        </ThemeProvider>
      </MUThemeProvider>
  )
}

export default MyApp

EmotionのThemeProviderとMUIのThemeProviderの名前が被っているので別名でimportして、それぞれで定義したthemeをprovideするようにしています。

こうすることで、アプリを起動した時にそれぞれのコンポーネントにthemeが提供され、各コンポーネントにて使用できるようになります

ここまで説明すると、薄々答えが見えてきた方もいると思いますが、Storybookの起動時はこのthemeを別途Provideする必要があるため、そのままStorybookを起動してしまうとtheme周りのエラーがでます。

Storybook

というわけで本編です。
AppText.tsxのstoriesがこんな感じになります。

AppText.stories.tsx
import { ComponentMeta } from '@storybook/react'
import AppText from './AppText'

export default {
  title: 'Components / Atoms / AppText',
  component: AppText,
} as ComponentMeta<typeof AppText>

export const Default = () => <AppText>Hoge</AppText>

この状態でStorybookを起動すると以下のようなエラーが出ます

Cannot read properties of undefined (reading 'text')

エラー自体はよくみるやつで、要は

undefinedにtextってやつはないよ

って言われていて、先ほど言ったようにStorybookではthemeをProvideしていないので、このようなエラーがでます。

じゃあthemeをProvideしようかとなるのですが、方法がいくつかあります。

方法1

方法1は各Storyにdecoratorsというものを定義する方法です
先ほどのStoryを以下のように修正します。

AppText.stories.tsx
import { ComponentMeta } from '@storybook/react'
import AppText from './AppText'

export default {
  title: 'Components / Atoms / AppText',
  component: AppText,
  // 追加
  decorators: [
    (Story) => (
      <MUThemeProvider theme={theme}>
        <ThemeProvider theme={theme}>
          <Story />
        </ThemeProvider>
      </MUThemeProvider>
    ),
  ],
  // ここまで
} as ComponentMeta<typeof AppText>

export const Default = () => <AppText>Hoge</AppText>

Storyの設定ではdecoratorsというものが設定でき、ここでthemeをProvideするようにすれば、実際にStorybookを起動した際にも、themeを提供することができます。

Storybookのdecorators自体は色んな用途で使用されるのですが、今回はThemeをProvideするという用途で使用しています

https://storybook.js.org/docs/react/writing-stories/decorators#context-for-mocking

ここまでが方法1なのですが、この方法の問題点としてはコンポーネントごとにdecoratorsを設定する必要があり、例えば今回のように不特定多数のコンポーネントでthemeが使用されるような場合だと、わざわざ使用されるStoryファイル全部にdecoratorsを書かないといけなくなります

まあ、そもそもコンポーネントの数が少ないとかだとそれでもいいんですが、対応漏れや冗長であることからGlobalな感じでdecoratorsを設定できると良いですね

ということで方法2です

方法2

といっても、書く場所が違うだけで、書くことはほぼ同じなのですが、、、
.storybook/preview.jsという場所(なければ作ります)に以下のコードを追加します

preview.js
// 略

export const decorators = [
  (Story) => (
    <MUThemeProvider theme={theme}>
      <ThemeProvider theme={theme}>
        <Story />
      </ThemeProvider>
    </MUThemeProvider>
  ),
]

方法1で書いたものと全く同じですが、preview.jsにてdecoratorsとして変数をexportすることで全Storyファイルにこのdecoratorsが適用されます。

こうすることで、わざわざ新しいコンポーネントを作ってStoryを定義しようとした時に、ThemeProviderやthemeをimportしてdecoratorsに書く必要がなくなりました!

まとめ

今まで何度かStorybookを使用したことはあったのですが、今回初めてEmotionなどを含めた構成をはじめから構築したので、色んなところで躓きましたがいい経験になりました

結構Theme系は調べると奥が深いのでこれを機にもっと知識を深めていきたいですね

Discussion