Next.js+TypeScript 環境で Storybook を使う
1年ほどコードを書く業務から離れていたらだいぶ腕がなまっていて焦りながら学習をしています。neotag です。
久しぶりに Next.js 環境を最初から立ち上げていたら手元のメモが増えてきたのでちょっとずつブログにしていこうと思います。ポエムネタが切れてきたわけではないです。はい・・・。
今回はまっさらな Next.js+TypeScript 環境に Storybook を入れようとしてドキュメントを眺めていたら少しハマったのでできるだけ単純化してまとめます。
デモは下記に push してあります。
ちなみに答えはすべて下記に書いてありました。設定をみたいだけの方はこの記事よりも公式を見たほうが良いと思います。
- https://github.com/vercel/next.js/blob/18a9c7e371efc4c487f9c3599c3211ce30009d6c/examples/with-storybook/.storybook/config.js
- https://storybook.js.org/docs/configurations/typescript-config/#setting-up-typescript-with-babel-loader
Next.js + TypeScript の環境を立ち上げる
Next.js は進化がはやくて毎回使うたびにちょっとずつ手順が変わっているような気がします。
ということでまずはゼロから Next.js をインストールしてみます。
Next.js のインストール後に TypeScript 化をおこないます。公式ドキュメントとチュートリアルの下記あたりが参考になります。というかそのままです。
- https://nextjs.org/docs/getting-started#setup
- https://nextjs.org/docs/basic-features/typescript
- https://nextjs.org/learn/excel/typescript
ここでの作業は下記の PR にまとめてあるのでよければあわせてご参照ください。
ちなみに今回つかったパッケージは以下の通りです。
"dependencies": {
"next": "9.4.4",
"react": "16.13.1",
"react-dom": "16.13.1"
},
"devDependencies": {
"@storybook/addon-links": "^5.3.19",
"@storybook/react": "^5.3.19",
"@types/node": "^14.0.13",
"@types/react": "^16.9.38",
"babel-loader": "^8.1.0",
"babel-preset-react-app": "^9.1.2",
"typescript": "^3.9.5"
}
create next-app
まずは create next-app
を実行します。僕は普段 yarn を使っているので下記を実行します。
$ yarn create next-app app_name
このとき使用している Node のバージョンが古いと下記のように怒られます。今回は手元にたまたま入っていた v14.4.0
を使用します。
error watchpack@2.0.0-beta.13: The engine “node” is incompatible with this module. Expected version “>=10.13.0”. Got “10.12.0”
また、yarn create next-app ./
のように現在のディレクトリを指定できるのですが、その際に .node-version
があると create next-app
中に conflict を起こします。 .node-version
でバージョン指定をする際は一階層上のディレクトリに置くなりしましょう。
The directory app_name contains files that could conflict:
.node-version
正しく create next-app
が実行できると template を使用するか聞かれます。ここでまっさらの App を作るか公式の Example を元に App を作るか選べます。
今回は Default starter app
を選びました。
Pick a template › - Use arrow-keys. Return to submit.
❯ Default starter app
Example from the Next.js repo
TypeScript を有効にする
素のままで create next-app
をした場合 TypeScript が無効になっているので有効にします。yarn
や yarn dev
で使われている next
コマンドは tsconfig.json
の有無を見て TypeScript を使用するか判断しているようです。
まずは必要なパッケージをインストールし
$ yarn add -D typescript @types/react @types/node
空の tsconfig.json
を作成します。
$ touch tsconfig.json
この状態で next
コマンドを実行すれば TypeScript が有効になります。
$ yarn dev // 中身は next dev です。
すると、tsconfig.json
が更新され next-env.d.ts
が生成されます。
TypeScript で Next.js を transpile する際に使われるデフォルトの設定と必要な型定義ファイルが出力されました。
以前は ts 化も一苦労したような記憶がありますが便利ですね。しあわせ。
tsx ファイルを作る
デフォルトの Next.js では App コンポーネントを記述した _app.js
は隠蔽されているので編集したい場合は作成する必要があります。
また create next-app
で生成された pages/index
は .js
ファイルなので .tsx
ファイルに差し替えます。(やらなくてもいいのですが、混在するのはいやなので。)
_app.tsx
Redux などを使うときに App の修正が必要なので Storybook には関係ないですが追加しておきます。
(そういえば arrow function にし忘れてますね。lint 設定してないので見落としていました。)
import { AppProps } from "next/app";
function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
export default App;
参照元: https://nextjs.org/learn/excel/typescript/nextjs-types
pages/index.tsx
Next.js 的には拡張子を .js
から .tsx
に変えるだけで十分なのですが、デフォルトのままだと Storybook から読んだときに React の import が出来ずビルドに失敗するので一部編集します。
diff --git a/pages/index.tsx b/pages/index.tsx
index 0303970..97eebee 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,4 +1,4 @@
-import { FC } from "react";
+import React, { FC } from "react";
import Head from "next/head";
const Home: FC = () => {
Storybook の WebPack 設定を Next.js に寄せられると良いと思うのですが、普段使っている Lint でも React を明示するように怒られるので気にせずこのままにしています。
これで Next.js + TypeScript 環境はできました。あらためて yarn dev
してみましょう。
$ yarn dev
locahost:3000 で下記ページが表示されれば Next.js の準備は完了です。
Storybook の設定
すでに Next.js が立ち上がっているので極力 package.json
を肥大化させず Next.js にあわせた形で Storybook をインストールします。
Next.js のリポジトリには examples/with-storybook
というテンプレートがあるのですが、これは ts 化されていないので参考にしつつ追加の設定が必要になります。
ここでの作業は下記の PR にまとめてあるのであわせてご参照ください。
Storybook のインストール
使用するパッケージ一式をインストールします。
Next.js は transpile に babel-loader
を使っているようなのであわせます。(ここちょっと自信ないので間違っていること書いているかもしれません。)
$ yarn add -D @storybook/react @storybook/addon-links babel-loader babel-preset-react-app
設定ファイルの追加
.storybook
に設定を追加していきます。この設定が分からずハマりました。
.storybook/config.js
前述の examples/with-storybook
を参考に config.js
を作成します。
基本的には下記と同じですが、pages/
と components/
以下の *.story.{js,ts}
と .stories.{js,ts}
を対象にするようにしています。
// via: https://github.com/vercel/next.js/blob/18a9c7e371efc4c487f9c3599c3211ce30009d6c/examples/with-storybook/.storybook/config.js
import { configure, addParameters } from "@storybook/react";
addParameters({
options: {
storySort: (a, b) => {
// We want the Welcome story at the top
if (a[1].kind === "Welcome") {
return -1;
}
// Sort the other stories by ID
// https://github.com/storybookjs/storybook/issues/548#issuecomment-530305279
return a[1].kind === b[1].kind
? 0
: a[1].id.localeCompare(b[1].id, { numeric: true });
},
},
});
// automatically import all files ending in *.stories.js or *.story.js
const req = [
require.context("../pages", true, /.stor(ies|y).[tj]sx$/),
require.context("../components", true, /.stor(ies|y).[tj]sx$/),
];
// the first argument can be an array too, so if you want to load from different locations or
// different extensions, you can do it like this: configure([req1, req2], module)
configure(req, module);
.storybook/main.js
webpack の設定がわからずハマっていましたが、Next.js が babel-loader
を使っているっぽかったので、下記の Storybook 公式ドキュメントに沿って main.js
を作成しました。
// from: https://storybook.js.org/docs/configurations/typescript-config/#setting-up-typescript-with-babel-loader
module.exports = {
stories: ["../{pages,components}/**/*.stor{ies,y}.{t,j}sx"],
webpackFinal: async (config) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve("babel-loader"),
options: {
presets: [["react-app", { flow: false, typescript: true }]],
},
});
config.resolve.extensions.push(".ts", ".tsx");
return config;
},
};
ついでに .storybook/addon.js
後述のサンプル用の story で @storybook/addon-links
を使うので addon.js
も追加します。
import "@storybook/addon-links/register";
サンプル追加
Storybook の Quick Start Guide どおりに実行しているとよく目にするデモの story を追加してみます。
components/storybook_demo/button.story.tsx
conponents/
配下においてみます。
import React from "react";
import { Button } from "@storybook/react/demo";
export default { title: "Demo Button" };
export const withText = () => <Button>Hello Button</Button>;
export const withEmoji = () => (
<Button>
<span role="img" aria-label="so cool">
😀 😎 👍 💯
</span>
</Button>
);
pages/storybook_demo/welcome.story.tsx
こちらは動作確認もかねて components/
ではなく pages/
配下におきます。
import React from "react";
import { linkTo } from "@storybook/addon-links";
import { Welcome } from "@storybook/react/demo";
export default { title: "Welcome" };
export const toStorybook = () => <Welcome showApp={linkTo("Demo Button")} />;
storybook コマンドの追加と実行
package.json
に scripts
に storybook
を追加します。
Next.js では /public
フォルダーにスタティックファイルが入っているので -s
オプションを指定します。.tsx
から import
していれば問題ないのですが、Home
内にある <img src="/vercel.svg" alt="Vercel Logo" className="logo" />
のような指定方法で404が出るので忘れずに指定します。
start-storybook
は引数なしで実行すると毎回ランダムで port を決定するので -p
オプションでポートを固定します。ポート番号は伝統的(?)に Storybook で指定されている 6006
を使用します。
"scripts": {
...
"storybook": "start-storybook -s ./public -p 6006"
...
},
余談ですが、 start-storybook
は指定されたポートがすでに使われている場合ポート番号をインクリメントして空いている番号(この場合6007)を使うか提案してくれます。丁寧でとてもかわいいやつですね。
無事立ち上がったら localhost:6006 を確認します 🎉 @storybook/addon-links
を有効にしたので Welcome To Storybook にある stories リンクもちゃんと機能していますね。
pages/index を story に追加してみる
pages/
以下の Container Component を story に追加するのは Storybook の使い方としてどうなんだという気がしないでもないですが、 create next-app
で生成された Home
(pages/index) を Storybook に表示してみます。
pages/index.story.tsx
import React from "react";
import Home from "./index";
export default { title: "Welcome" };
export const toNext = () => <Home />;
これで Next.js のページも Storybook に追加できました 🎉
今 Next.js の開発元が提供している Vercel というフロントエンド向けのホスティングサービスで遊んでいて、本当は Storybook と Next.js を Vercel にデプロイするところまでやりたかったので地味にハマり中でこの記事の締め切りまでにたどりつけませんでした。
解決したら Vercel にデプロイするところもまとめたいと思っています。
Storybook が好き
元々HTMLコーダーからマークアップエンジニアを経て、最近はフロントエンドのコードを書いたり社内の雑務をやったりしています。
その経歴もあってコンポーネント単位で関心の分離ができている疎結合なコンポーネントを作るのが好きです。そしてそれをスムースに実行できる storybook が大好きです。
最近は CSS in JS の普及などでコンポーネント開発はぐっと楽になりました。その分コンポーネント管理に割ける余力が増えればコンポーネントのカオス状態から脱して人間が十分に管理できる粒度のコンポーネントを維持できるのではと期待しています。
そのために Storybook のような可用性のたかいコンポーネントを管理できるシステムをより活用していきたいですね。
開発のお悩み、フロントエンドから解決しませんか?
あなたのチームのお悩みはなんですか?
「バックエンドエンジニアにフロントエンドまで任せてしまっている」
「デザイナーに主業務以外も任せてしまっている」
「すべての手が足りず細かいことまで手が回らない」
役割や領域を適切に捉えてカバーし、チーム全体の生産性と品質をアップするお手伝いをします。
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にお声がけください。
オンラインでのヒアリングとフルリモートでのプロセス支援に対応しています。
リモートワーク対応のパートナーをお探しの場合もぜひ弊社にお問い合わせください!
お悩み相談はこちらから!