社会課題を解決する新規事業立ち上げに取り組んでいる @aoyagi です。
BizDev 側の作業の合間に開発環境をアップデートしたのでその知見を共有します。
なんでも1人でやらなければならないのが新規事業開発の辛いところですが、肝心のプロダクトをスピーディに作っていく上でも快適な開発環境は欠かせません。
Angular を書いていた頃は Karma で UI のテストをしていたのですが、React(Next.js) を書くようになってからは Storybook を利用しています。
先日 Storybook のバージョン7がリリース されましたが、リリース前から先行して v6.5 からアップデートしておりその時に詰まったところもあったのでざっくりと解説します。
サンプルプロジェクトも公開しましたので記事で不足している部分はこちらを参照してください。
2023年8月27日更新
本記事やサンプルコードは Next.js 13.4 から安定版となった App Router
には対応していません。Material UI も今後は App Router が推奨となっていくようですのでいずれ検証したいと思います。
2023年10月22日更新
addon-styling
が廃止されていたので addon-themes
に変更しました。
2024年2月25日更新
Material UI を Next.js に統合できるパッケージ @mui/material-nextjs
に対応しました。
Emotion 周りの面倒な設定が一切不要になりました。
Next.js
と Material UI
のセットアップ
Material UI の GitHub リポジトリにある examples にサンプルがありますので、サンプルに従って _app.tsx
_document.tsx
を作成します。
個人的には Typescript
をオススメしますが、Javascript 版も同階層にあります。
Material UI はバージョン5からスタイリングに Emotion が利用されるようになっていますので関連した設定が必要なのでコピー&ペーストしましょう。
さらに今回はダークモードのテストも兼ねてシステム設定によるテーマ切替を追加してみます。
フォルダ名は任意で構いませんが、themes
フォルダにライトテーマとダークテーマをそれぞれ作成します。
themes/
├── index.ts
├── light.theme.ts
└── dark.theme.ts
index.ts
では Next.js 13 から利用できるようになったフォントのセルフホスティング機能をさっそく使ってみます。
next/font/google
を使って利用するフォントをインポートします。
フォントはビルド時にダウンロードされセルフホスティングされるのでパフォーマンスとプライバシーを向上させることができます。
import { Roboto } from 'next/font/google';
import { lightTheme } from './light.theme';
import { darkTheme } from './dark.theme';
export const roboto = Roboto({
weight: ['300', '400', '500', '700'],
subsets: ['latin'],
display: 'swap',
fallback: ['Helvetica', 'Arial', 'sans-serif'],
});
export const themes = {
light: lightTheme,
dark: darkTheme,
};
export default themes;
light.theme.ts
と dark.theme.ts
ではそれぞれ createTheme
でテーマを作成しているだけなので説明は割愛します。
_app.tsx
で useMediaQuery
を使用し、OSの外観モードに合わせてテーマが切り替わるようにします。
// ...
export default function MyApp(props: MyAppProps) {
// ここから
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const theme = React.useMemo(
() => prefersDarkMode ? themes.dark : themes.light,
[prefersDarkMode],
);
// ここまで
return (
<AppCacheProvider {...props}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</AppCacheProvider>
);
}
// ...
_document.tsx
でも PWA アプリにした際のツールバーの色をテーマに合わせるために同じ記述を加えています。
(冗長なので共通化した方が良いですがここでは分かりやすさを優先しています)
余談ではありますが以前は Head
に <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
という記述がありましたが今ではなくなっています。
Storybook
のセットアップ
Storybook v6.5から v7 へのアップデート方法は公式に重厚なドキュメントが公開されています。
私も全ては読みきれていませんが、手っ取り早く Next.js + Material UI でコンポーネントがテストできる状態を目指します。
v7 が正式にリリースされる前はアップグレードには npx storybook@latest upgrade --prerelease
と --prerelease
オプションが必要でしたが現在では不要なはずです(試していませんが)。
実行すると対話式でアップグレードが実行されますが、.storybook/main.js
等のファイルが大きく書き変わりますのでご注意ください。
また、自動アップグレードをしても私の場合は以下の依存関係が不足していたので追加で実行する必要がありました。
npm i -D css-loader style-loader util
Storybook の設定ファイルは以下の通りです。
2023年5月更新
以下のリポジトリを参考にさせていただきました。
https://github.com/Integrayshaun/storybook-mui-example
アドオンを利用すると preview.js
をシンプルに記述できるようなので試してみました。
npm i -D @storybook/addon-styling
const path = require('path');
const toPath = (filePath) => path.join(process.cwd(), filePath);
const config = {
webpackFinal: async (config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
'@': path.resolve(__dirname, '../src'), // 環境によって異なります
},
},
}
},
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-mdx-gfm", // MDX を GitHub Flavored Markdown で表現したい場合は追加
"@storybook/addon-themes" // 追加
],
framework: {
name: "@storybook/nextjs",
options: {}
},
features: {
emotionAlias: false,
},
typescript: {
check: false,
checkOptions: {},
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
allowSyntheticDefaultImports: false, // speeds up storybook build time
esModuleInterop: false, // speeds up storybook build time
shouldExtractLiteralValuesFromEnum: true, // makes union prop types like variant and size appear as select controls
shouldRemoveUndefinedFromOptional: true, // makes string and boolean types that can be undefined appear as inputs and switches
propFilter: (prop) =>
prop.parent
? !/node_modules\/(?!@mui)/.test(prop.parent.fileName)
: true,
},
},
};
export default config;
私は Typescript のモジュールパスエイリアスを利用しているので Storybook も同様の設定をしています。
('@': path.resolve(...)
の記述)
この記述は環境によって異なりますので webpackFinal:...
の部分は削除するかお好みのパスに書き換えてください。
tsconfig-paths-webpack-pluginを使えば tsconfig ファイルからパスを呼び出せるようですが単純に合わせています。
Storybook のドキュメントの自動生成を有効にしたい場合1は以下を追加してください。
docs: {
autodocs: true
}
preview.js
はアドオンを利用することにより非常にシンプルになりました。
// .storybook/preview.js
import { CssBaseline, ThemeProvider } from '@mui/material'
import { withThemeFromJSXProvider } from '@storybook/addon-themes'
import { themes } from '@/themes'
// Load Roboto fonts
import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css'
import '@fontsource/material-icons'
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
expanded: true,
hideNoControlsWarning: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;
export const decorators = [
withThemeFromJSXProvider({
themes,
defaultTheme: 'light',
Provider: ThemeProvider,
GlobalStyles: CssBaseline,
}),
]
アドオンの詳しい利用方法についてはリンク先をご参照ください。
ここまで準備ができたら Storybook を起動しましょう。
localhost:6006
で Storybook が表示されます。
npm run storybook
コンポーネントの作成
最後にテスト用のコンポーネントを作成します。
Material UI の Basic card を改変して一部のプロパティを変更できるようにします。
タイトル
最小の横幅
ボタンをクリックした時の遷移先
ボタンの色
を可変のプロパティとして定義しています。
パターンを示すために適当に定義しているため実際の開発の参考にはしないでください。
import { Box, Button, Card, CardActions, CardContent, Typography } from '@mui/material';
import { NextLinkComposed } from './Link';
const bull = (
<Box
component="span"
sx={{ display: 'inline-block', mx: '2px', transform: 'scale(0.8)' }}
>
•
</Box>
);
interface MyCardProps {
title: string,
minWidth?: number,
path?: string,
color?: 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning'
}
export default function MyCard({ title, minWidth = 275, path = '/', color = 'primary' }: MyCardProps) {
return (
<Card sx={{ minWidth: minWidth }}>
<CardContent>
<Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
{title}
</Typography>
<Typography variant="h5" component="div">
be{bull}nev{bull}o{bull}lent
</Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary">
adjective
</Typography>
<Typography variant="body2">
well meaning and kindly.
<br />
{'"a benevolent smile"'}
</Typography>
</CardContent>
<CardActions>
<Button size="small"
color={color}
component={NextLinkComposed}
to={{
pathname: path
}}
>Learn More</Button>
</CardActions>
</Card>
);
}
コンポーネントを Storybook でテストするために stories
フォルダの下に *.stories.tsx
を作成します。
argTypes
ではプロパティを変更するためのコントロールを定義することができます。
チェックボックスや日付など色々なコントロールを指定できるので公式ドキュメントを参照してください。
args
ではコンポーネントに渡す初期値を設定できます。
// MyCard.stories.ts|tsx
import type { Meta, StoryObj } from '@storybook/react';
import MyCard from '@/components/MyCard';
const meta: Meta<typeof MyCard> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'MyCard',
component: MyCard,
argTypes: {
minWidth: {
control: { type: 'number', min: 275, max: 400, step: 5 }
},
path: {
control: 'text'
},
title: {
control: 'text'
},
color: {
options: ['inherit', 'primary', 'secondary', 'success', 'error', 'info', 'warning'],
control: { type: 'select' },
}
}
};
export default meta;
type Story = StoryObj<typeof MyCard>;
/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/7.0/react/api/csf
* to learn how to use render functions.
*/
export const Primary: Story = {
args: {
title: "Word of the Day",
color: "primary",
}
};
完成した Storybook は次のようになります。
実際のアプリケーションでは ライトモード
ダークモード
はOSの外観モードを変更しないと確認できませんが、Storybook 上で確認することができます。
また、コンポーネントのプロパティもコントロールで指定することでリアルタイムにデザインを確認できます。
(分かりにくくて申し訳ないですがボタンの色が変わってます)
今回の記事は以上となります。
ここでは紹介していませんが Storybook は Jest/Testing Library
や Cypress
Playwright
といったテストツールと連携でき Angular
や Vue
でも使える非常に強力なツールですので使いこなして開発を加速させましょう。
ここまでお読みいただきありがとうございました。
次回はバックエンド開発の記事を書きます。