🦜

Next.jsの環境を構築する(+TypeScript, Storybook, Hygen)

2021/07/14に公開

Next.jsの導入

typescriptを導入したい場合はオプションに--typescript(もしくは--ts)を追加するか、プロジェクト作成後に必要なファイルの作成およびパッケージのインストールを行います。

TypeScriptをプロジェクト作成時に導入
npx create-next-app project-name --typescript
TypeScriptをプロジェクト作成後に導入
npx create-next-app project-name
touch tsconfig.json
yarn add --dev typescript @types/react @types/node

プロジェクト作成後に下記を実行し、http://localhost:3000にアクセスできれば導入は完了です。

cd project-name
yarn dev

以降では、導入する際に設定しておくと良いことや、周辺ツールについてまとめていきます。
例として、下記のような最小構成のプロジェクトを用意します。

ディレクトリ構成(一部略)
project-name/
 ├── components/
 |	├── atoms/
 |	│   └── Button.tsx
 |	├── molecules/
 |	└── organisms/
 ├── pages/
 |	└── index.tsx
 ...
components/atoms/Button.tsx
interface Props {
  purpose?: string;
}

export const Button: React.FC<Props> = ({ purpose = "primary", children }) => {
  return (
    <button className={`button ${purpose}`}>
      {children}
      <style jsx>{`
        .button {
          width: 100px;
          padding: 4px 8px;
          border: none;
          border-radius: 4px;
          font-size: 16px;
        }

        .primary {
          background-color: #007bff;
          color: #fff;
        }

        .danger {
          background-color: #dc3545;
          color: #fff;
        }
      `}</style>
    </button>
  );
};
pages/index.tsx(Next.js導入時から存在するファイルの内容を書き換え)
import { Button } from "../components/atoms/Button";

export default function Home() {
  return (
    <div>
      <Button></Button>
    </div>
  );
}

モジュールをルート相対パスでimportできるようにする

参考:Absolute Imports and Module path aliases

開発を進めていくとモジュールをimportする機会も多くなると思いますが、その度に相対パスを辿るのはとても面倒です。また、相対パスでの記述は「import文を流用しにくい」「ディレクトリ構成の変更に弱い」など何かと不便なので、モジュールをルート相対パスでimportできるようにします。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": "." // モジュールをルート相対パスでimportできるようにする
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

tsconfig.json修正後は、モジュールをルート相対パスでimportできるようになります。

pages/index.tsx(Next.js導入時から存在するファイルの内容を書き換え)
- import { Button } from "../components/atoms/Button";
+ import { Button } from "components/atoms/Button";

export default function Home() {
  return (
    <div>
      <Button></Button>
    </div>
  );
}

Storybookの導入

Storybookはコンポーネントをカタログとして閲覧・管理できるツールです。

npx sb init

インストール後に下記を実行し、http://localhost:6006にアクセスできれば導入は完了です。

npm run storybook

atoms/Button.tsxのstoryを作成してみます。

stories/atoms/Button.stories.tsx
import React from "react";
import { Story, Meta } from "@storybook/react";
import { Button } from "components/atoms/Button";

export default {
  title: "Button",
  component: Button,
  argTypes: {
    purpose: { description: "ボタンの役割" },
  },
} as Meta;

export const Default: Story = () => <Button>Primary</Button>;

export const Danger: Story = () => <Button purpose="danger">Danger</Button>;

ただ、このままだと下記のようなエラーが発生します。
Next.jsではデフォルトで解決される絶対パスの設定が、storybookでは解決されないことによるエラーのようです。

ERROR in ./stories/atoms/Button.stories.tsx
Module not found: Error: Can't resolve 'components/atoms/Button' in 'D:\projects\sampleproject\stories\atoms'

これを解決するために、.storybook/main.jsを下記のように変更します。

.storybook/main.js
const path = require("path"); // 追加

module.exports = {
  stories: [
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
  framework: "@storybook/react",
  // ここから追加
  webpackFinal: async (baseConfig) => {
    baseConfig.resolve.modules = [
      ...(baseConfig.resolve.modules || []),
      path.resolve(__dirname, "../"),
    ];
    return baseConfig;
  },
  // ここまで追加
};

Hygenの導入

コンポーネントを作成する度に手動でstoryを作成しようとすると開発を進める過程で抜け・漏れが発生する可能性がありますし、そうでなくても単純に面倒です。

対話式のコードジェネレータであるHygenを使用することで、コンポーネント作成時にstoryも作成されるようにします。

yarn add -D Hygen

生成テンプレートを作成します。yarn hygen init selfで生成することもできますが、今回は自分で記述していきます。

project-name/
 ├── hygen/component/add
 |	├── index.tsx.t
 |	├── index.stories.tsx.t
 |	└── prompt.js
 .hygen.js
hygen/component/add/index.tsx.t
---
to: components/<%= level %>/<%= name %>.tsx
unless_exists: true
---
interface Props {
  purpose?: string;
}

export const <%= name %>: React.FC<Props> = () => {
  return (
    <>
      <style jsx>{`
      `}</style>
    </>
  );
};
hygen/component/add/index.stories.tsx.t
---
to: stories/<%= level %>/<%= name %>.stories.tsx
unless_exists: true
---
import React from "react";
import { Story, Meta } from "@storybook/react";
import { <%= name %> } from "components/<%= level %>/<%= name %>";

export default {
  title: "<%= name %>",
  component: <%= name %>,
  argTypes: {},
} as Meta;

export const Default: Story = () => <<%= name %>></<%= name %>>;
hygen/component/add/prompt.js
module.exports = {
  prompt: ({ inquirer }) => {
    const questions = [
      {
        type: "select",
        name: "level",
        message: "Which Atomic Design category?",
        choices: ["atoms", "molecules", "organisms"],
      },
      {
        type: "input",
        name: "name",
        message: "What is the component name?",
      },
    ];
    return inquirer.prompt(questions).then((answers) => {
      const { level, name } = answers;
      return { level, name };
    });
  },
};

index.tsx.t, index.stories.tsx.tはそれぞれコンポーネントとstoryのテンプレートファイル、prompt.jsは対話式プロンプトを定義するファイルです。

生成テンプレートを読み込むための設定ファイルも用意します。

.hygen.js
module.exports = {
  templates: `${__dirname}/hygen`,
};

最後に、npm scriptsを追加します。

{
  "scripts": {
    "hygen:add": "hygen component add"
  },
}

実行してみます。

$ yarn hygen-add
yarn run v1.22.10
$ hygen component add
? Which Atomic Design category? ... 
> atoms
  molecules
  organisms
? What is the component name? » MyButton

Loaded templates: hygen
       added: stories/atoms/MyButton.stories.tsx
       added: components/atoms/MyButton.tsx
Done in 2.90s.
components/atoms/MyButton.tsx
interface Props {
  purpose?: string;
}

export const MyButton: React.FC<Props> = () => {
  return (
    <>
      <style jsx>{`
      `}</style>
    </>
  );
};
stories/atoms/MyButton.stories.tsx
import React from "react";
import { Story, Meta } from "@storybook/react";
import { MyButton } from "components/atoms/MyButton";

export default {
  title: "MyButton",
  component: MyButton,
  argTypes: {},
} as Meta;

export const Default: Story = () => <MyButton></MyButton>;

生成されました。テンプレートファイルや対話式プロンプトは必要に応じて修正してください。

tailwindcssの導入

npm install tailwindcss postcss@latest autoprefixer@latest 
npm install @storybook/addon-postcss
npx tailwindcss init -p
tailwind.config.js
module.exports = {
  // 最小設定では不要の認識だが設定しないとtailwindcssが反映されない
  purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
}
styles/global.css
// ここから追加
@tailwind base;
@tailwind components;
@tailwind utilities;
// ここまで追加

storybookとの連携

storybookでpostcssを使用するように.storybook/main.jsに追加します。

.storybook/main.js
const path = require("path");

module.exports = {
  stories: [
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    // ここから追加
    {
      name: "@storybook/addon-postcss",
      options: {
        postcssLoaderOptions: {
          implementation: require("postcss"),
        },
      },
    },
    // ここまで追加
  ],
  framework: "@storybook/react",
  webpackFinal: async (baseConfig) => {
    baseConfig.resolve.modules = [
      ...(baseConfig.resolve.modules || []),
      path.resolve(__dirname, "../"),
    ];
    return baseConfig;
  },
};

storybookがstyles/global.cssを読み込むように、.storybook/preview.jsにコードを追加します。

storybook/preview.js
import "styles/globals.css";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

Discussion