目次
- Next.jsにStorybookを導入してTypeScriptで書けるようにする
-
Next.js + Storybookが動くまでに発生したエラーと解消法を発生順に列挙していく
- 「Module parse failed: Unexpected character ’@’ (1:0)」でSCSSが読み込めない
- 「Module not found: Error: Can’t resolve ‘src/hooks/useCounter.ts’」で絶対パスでのインポートができない(Absolute imports)
- 「Cannot read property ‘publicRuntimeConfig’ of undefined」でpublicRuntimeConfigから値を取得できない
- 「Cannot read property ‘pathname’ of null」でuseRouterが読み込めない
- _app.tsxで読み込んでいるスタイルが当たらない
- Google Analyticsをモックする
- Storybookのコンポーネントを複数のディレクトリから読み込む
- StorybookでuseContext(Context API)を使えるようにする
- Storybookで使えるstoreのモックを作成する
- まとめ
Next.jsにStorybookを導入してTypeScriptで書けるようにする
この記事では、Next.jsにStorybookを導入してTypeScriptでReactコンポーネントを書けるようにする手順を紹介します。またその際に、私が踏み抜いたバグと解消法を全て共有します。
Next.jsとは、Vercelが作成しているReactのフレームワークです。SSRにも対応しており、Reactで開発するならNext.jsかFacebook製のCreate React Appを使うのがスタンダードになっています。また、面倒な設定を書かなくてもすぐに使えるZero Configを標榜しており、実際にWebpackやTypeScriptと一緒にReactを書く際にも特別な準備は不要です。
Storybookとは、UIコンポーネントのカタログを作るツールです。Storybookの実行環境はメインのアプリケーションとは独立しているため、UI作成時に試行錯誤をしてもメインのアプリに影響を及ぼさないのが大きなメリットです。Storybookはエンジニアとデザイナーの橋渡しをしてくれるツールであり、ReactやVue、Angularなどコンポーネント指向のフレームワークと併用することが多いです。
Next.jsで作ったアプリケーションがリリース済みで本番稼働中であったため、Storybookの導入は一筋縄ではいかなかったです。Next.js公式のStorybookの導入サンプルは序章に過ぎなかったんや…。

Next.jsのExamplesのページ には、StorybookをJSで表示するものと、TypeScriptを使うもの2種類のサンプルがあります。今回の設定はTypeScript版を参照したよ。
それでも頑張ってNext.js + TypeScriptの環境ではStorybookが動作するところまで持っていったので、同じようなエラーを踏んで困っている方のお役に立てれば幸いです。
前準備
Next.jsにTypeScriptを導入する
Next.jsのプロジェクトのセットアップが終わっているとします。TypeScriptとReact、Node.jsの型をインストールします。
$ npm install --save-dev typescript @types/react @types/node
次に、ディレクトリルートにtsconfig.json
を作成して、以下のように記載します。
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
Next.jsにTypeScriptの導入する手順は以上です。
Next.jsにStorybookを導入する
Next.jsにStorybookを導入しましょう。今回はアドオンは追加しません。
$ npm install -D @storybook/react @storybook/preset-typescript
Storybookのインストールを終えたら、以下のコマンドをpackage.json
に追加します。
{
"scripts": {
// ...
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}
}
次に、ローダーを追加します。
$npm install -D babel-loader @babel/core ts-loader
Next.jsのプロジェクトルートに.storybook
ディレクトリを作成し、main.js
を追加します。
module.exports = {
addons: ['@storybook/preset-typescript'],
}
また、同ディレクトリにpreview.js
を追加し、Storybook用コンポーネントでTypeScriptのファイル*.stories.tsx
を読み込む記述を追加します。
import { configure } from '@storybook/react'
const req = require.context('../stories', true, /.stories.tsx?$/)
configure(req, module)
以上で準備ができました。
Next.js + Storybookが動くまでに発生したエラーと解消法を発生順に列挙していく
$ npm run storybook
を実行するとStorybookが立ち上がり、http://localhost:6006
で表示できます。
しかし、現時点でコンポーネントは何も表示されません。そこであえて一番複雑なコンポーネントを表示しようと思ったら、見事にたくさんのエラーに遭遇しました。
なぜなら、Next.jsでReactのuseContext(Context API)を使っていたり、Next Routerを使っていたり、Google Analyticsを設定していたり、CSS Modulesを使っていたからです。

$ npm run storybook を実行するたびにエラーが発生したので、1つずつ潰していったんだ。Next.jsの機能をフル活用している稼働中のアプリならではのエラーだったよ。
以下ではそれぞれのエラー内容、エラーの原因、解決法を解説していきます。
「Module parse failed: Unexpected character ’@’ (1:0)」でSCSSが読み込めない
Next.jsは9.3からCSS Modulesをサポートしています。この機能を使うと、Next.jsがビルドするReactコンポーネントからSCSSを読み込めます。
import styles from './Header.module.scss'
export function Header() {
return (
<nav className={styles.error}>
<p>Header</p>
</nav>
)
}
しかし、StorybookがSCSSを読み込めないため、以下のエラーが表示されます。
Module parse failed: Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type,
currently no loaders are configured to process this file.
See https://webpack.js.org/concepts#loaders
> @import '../../Assets/scss/lib/color';
| @import '../../Assets/scss/lib/variable';
| @import '../../Assets/scss/lib/icon';
Next.js + StorybookでSCSS(CSS Modules)を使うために、CSSに関するローダーを追加します。
$ npm install -D sass css-loader sass-loader style-loader
次に、StorybookのWebpackでこれらのローダーを使えるようにします。main.js
に以下の設定を追加します。
module.exports = {
addons: ['@storybook/preset-typescript'],
webpackFinal: async (baseConfig) => { // scss を読み込む baseConfig.module.rules.push({ test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1, // 1 => postcss-loader modules: { localIdentName: '[local]___[hash:base64:2]', }, }, }, 'sass-loader', ], }); return {...baseConfig}; }}
これでSCSSが読み込めない解消されました。
「Module not found: Error: Can’t resolve ‘src/hooks/useCounter.ts’」で絶対パスでのインポートができない(Absolute imports)
Next.jsの9.4から、モジュールを絶対パスでインポートができるようになりました。
import useCounter from 'src/hooks/useCounter'
export function Counter() {
const [count, increment] = useCounter()
return
<>
<p>{count}</p>
<button onClick={() => increment()}>
+
</button>
</>
)
}
この機能を使っていると、以下のエラーが表示されました。
ERROR in ./src/Components/Header/Header.tsx
Module not found: Error: Can’t resolve ‘src/hooks/useCounter.ts’
in ‘/Users/panda/nextjs/app/src/Components/Header’
Storybookでも絶対パスでモジュールをインポート(Absolute imports)するためには、.storybook/main.js
に以下の記述を追加します。
module.exports = {
addons: ['@storybook/preset-typescript'],
webpackFinal: async (baseConfig) => { // @see https://github.com/storybookjs/storybook/issues/3916#issuecomment-407681239 baseConfig.resolve.modules = [ ...(baseConfig.resolve.modules || []), path.resolve('./'), ]
// scss を読み込む
// ...
}
}
これで絶対パスでモジュールを読み込むことができました。(参考: I can not import with absolute path in Storybook)
「Cannot read property ‘publicRuntimeConfig’ of undefined」でpublicRuntimeConfigから値を取得できない
publicRuntimeConfigを使うことにより、Next.jsでnext.config.jsから実行時に値を読み取れます。
module.exports = {
serverRuntimeConfig: {
// serverでの利用可能
mySecret: 'secret',
secondSecret: process.env.SECOND_SECRET,
},
publicRuntimeConfig: {
// serverとclient両方で利用可能
appHost: 'next-storybook-app.com',
},
}
これでconfig.ts
でappHost
の値を取得できます。
import getConfig from 'next/config'
const { publicRuntimeConfig } = getConfig()
const { appHost } = publicRuntimeConfig
しかし、この機能を使っていると、Storybookで以下のエラーが表示されました。
「Cannot read property ‘publicRuntimeConfig’ of undefined」は、.story/preview.js
に以下のように記述することで解決できます。
import { setConfig } from 'next/config';
import { publicRuntimeConfig } from '../next.config';
setConfig({ publicRuntimeConfig });
また、publicRuntimeConfigを呼び出しているconfig.ts
も以下のように書き換えます。
import getConfig from 'next/config'
const { publicRuntimeConfig = {} } = getConfig() || {}const { appHost } = publicRuntimeConfig
これでStorybookでNext.jsのpublicRuntimeConfigを使っているファイルを読み込めました。(参考: publicRuntimeConfig undefined when using Storybook with Next.js)
「Cannot read property ‘pathname’ of null」でuseRouterが読み込めない
Next.jsでは、React HooksのuseRouterを使うとURLのpathnameやqueryを値として取得できます。
import { useRouter } from 'next/router'
export function App() {
const { pathname } = useRouter()
return <p>{pathname}</p>
)
}
しかし、このままではStorybookで「Cannot read property ‘pathname’ of null」というエラーが表示されます。
そこで、.story/preview.js
に以下のように記述しNext.jsのuseRouterをモックすることでエラーを解決できます。
import * as nextRouter from 'next/router'
// ダミーデータは適宜変更する
nextRouter.useRouter = () => ({
route: "/",
pathname: "/about",
query: { query: 'Next.js Storybook' },
asPath: "",
basePath: "",
})
また、Routerオブジェクトを使っている場合は以下のようにモックします。
import * as nextRouter from 'next/router'
import Router from 'next/router'
nextRouter.useRouter = () => ({
// ...
})
nextRouter.router = { push: () => {}, prefetch: () => new Promise((resolve, reject) => {}),};
これで、StorybookでNext.jsのRouterをMockできました。(参考: How to mock useRouter?、How to mock next/router in Storybook)
_app.tsxで読み込んでいるスタイルが当たらない
普段_app.tsx
で読み込むようなscssファイルは、preview.jsで読み込むことでStorybookの各コンポーネントに適用されます。
import '../src/assets/scss/style.scss'
Google Analyticsをモックする
Next.jsでGoogle Analyticsを使っている場合は、gtagモックをpreview.js
に記述します。
window.gtag = () => {}
関連記事: Next.jsでGoogle Analyticsを使えるようにする
これでGoogle Analyticsのイベントをモックできました。
Storybookのコンポーネントを複数のディレクトリから読み込む
Storybookのコンポーネントを複数のディレクトリから読み込むユースケースはそれほど多くないと思いますが、方法を記載しておきます。
preview.js
にstories.tsxファイルがあるコンポーネントを記載するだけです。
const req1 = require.context('../src', true, /.stories.tsx?$/)
const req2 = require.context('../stories', true, /.stories.tsx?$/)
configure([req1, req2], module)
StorybookでuseContext(Context API)を使えるようにする
useContextを使っているコンポーネントをStorybookから呼び出したい場合は、StorybookのDecoratorを利用します。このDecoratorを使って、Contextを利用するコンポーネント(Consumer)をProviderでラップします。
Headerコンポーネントでstoreからユーザー名を取得しているケースを例に解説します。
import React, { useContext } from 'react'
import { Context } from 'src/lib/store/context'
type Props = {
username: string | null
loggedIn: boolean
}
const Component: React.FC<Props> = (props) => (
<div>
Hello,{' '}
{props.loggedIn ? `${props.username}` : 'ゲスト' }さん
</div>
)
const Container: React.FC<{}> = () => {
const { state } = useContext(Context)
const loggedIn = state.user.loggedIn === true
return <Component
username={state.user.name}
loggedIn={loggedIn}
/>
}
Container.displayName = 'Header'
export default Container
Storybook側のHeader.stories.tsx
は以下のようになります。
import React from 'react'
import Header from './Header'
import { Context } from 'src/lib/store/context'
export default {
title: 'Header',
}
export const header = () => <Header />
const store = {
state: { user: { name: 'パンダ', loggedIn: true } },
dispatch: () => {},
}
header.story = {
decorators: [storyFn =>
<Context.Provider value={store}>
{storyFn()}
</Context.Provider>
]
}
これでStorybookでuseContextを使っているReactコンポーネントを描画できました。
Storybookで使えるstoreのモックを作成する
上記でStoreをモックできましたが、毎回全てのコンポーネントにdecoratorを記述するのは面倒です。
Header.stories.tsx
の中で、「非ログイン状態」と「ログイン済みでユーザー名がある状態」の2つのコンポーネントを作るときには、2つのdecoratorを記述しなければなりません。
しかも、コンポーネントの数はある状態の数×別の状態の数であるように、状態(state)の数の掛け算で増えていきます。
何度も同じコードを書くことは「繰り返しを避ける」というDRY原則に違反します。この問題を解決するために、Storeに格納する値をStorybookのコンポーネント側で自由に設定できるようなラッパー関数を作りました。
import React from "react";
import { Context, Store } from "../../src/Lib/store/context";
import { initialState } from "../../src/Lib/store/reducer";
type State = typeof initialState
// initialStateと外から与えられた値をマージする
const mockState = (state): State => ({...initialState, ...state})
const mockContextValue = (state): Store => ({
state: mockState(state),
// dispatcherをモックする
dispatch: () => {},
})
export const withStore =
(comp: React.ReactElement, state: Partial<State> | {} = {} ) => {
const Component = () => comp
// Providerでラップする
Component.story = {
decorators: [storyFn =>
<Context.Provider value={mockContextValue(state)}>
{storyFn()}
</Context.Provider>
],
}
return Component
}
withStore
関数を使うことで、HeaderコンポーネントにモックのStoreを渡すだけで様々な状態を表現できるようになりました。
import React from 'react'
import Header from './Header'
import { Context } from 'src/lib/store/context'
export default {
title: 'Header',
}
// 非ログイン
const guestStore = {
user: {
loggedIn: false
name: null
},
}
export const guest = withStore(<Header />, userSearchStore)
// ログイン済み
const userStore = {
user: {
loggedIn: true
name: 'パンダ'
},
}
export const guest = withStore(<Header />, userSearchStore)
ぜひ使ってみてください。
まとめ
エラーの発生時とエラーの解消なんて簡単なものですよ。未知のエラーに遭遇した時は、以下のステップをたどるだけです。
- 未知のエラーが発生する
- エラー文をコピーしてGoogle検索する
- GitHubのissue、もしくはStack Overflowを読む。なければissueを立てる
- スレッドの最初(問題提起)と流れを把握し、一番リアクションの多いリプライを読む
- 解決策を手元で試す
- 解決する
- 次のエラーが発生。1に戻る
この手順は歴としたLoop文ですね。breakがないからLoopを抜けられない?その通りです。あなたがエンジニアである限りはね。
我々にあるのはcoffee breakだけです。

......。お後がよろしいようで