【始め方】Next.js + Material-UI + styled-components + TypeScript + Storybook + ESLint + prettier + Vercel の快適開発

## こちらをご覧ください
最新の Next.js での環境構築について記事を新しく書いたので、こちらをご覧ください。

https://qiita.com/hasehiro0828/items/ef1736e871b85b039212


## はじめに

以前[React 開発時にやること](https://qiita.com/hasehiro0828/items/04d7a7f8c0beb8ca4fa1)という記事を書いたのですが、
今回 [Next.js](https://nextjs.org/) を使ったので、同じように最初にやるべきことを記録しておきます。

以下に書いていることを実行するだけで、 Next.js を簡単にいい感じに始められますが、
一度は公式ドキュメントに目を通してみることをお勧めします。
英語ですが結構読みやすく、Next.jsの概念を掴むことができました。

https://nextjs.org/learn/basics/create-nextjs-app

## この記事を実行すると出来上がるもの
https://github.com/hasehiro0828/-QP-next-template

## 元となるコードを生成

```bash
yarn create next-app app
```

### typescript等ダウンロード & tsconfig.json の追加

```bash
cd app

yarn add -D typescript @types/react @types/node
touch tsconfig.json
```

### tsconfig.json等の自動更新

```bash
yarn dev
# tsconfigが自動で設定されているのを確認したら、コントロール + C で一旦停止
```

### tsconfig.json の修正 (`strict`がデフォルトで`false`になっている)

```json
{
  "compilerOptions": {
   	...
    "strict": true,
    ...
  }
}

```

## Lint & Formatter の追加

```bash
yarn add -D eslint prettier eslint-config-prettier eslint-config-airbnb-typescript
```

### eslintの初期化

```bash
./node_modules/.bin/eslint --init

## 以下選択肢 ##
✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · react
✔ Does your project use TypeScript? · Yes
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · guide
✔ Which style guide do you want to follow? · airbnb
✔ What format do you want your config file to be in? · JavaScript
✔ Would you like to install them now with npm? · Yes
###########

# yarnを使うので、不要ファイル削除
yarn install
rm -rf package-lock.json
```

### prettierとeslintの統合

eslintと統合するため、`.eslintrc.js`を修正

```js:.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    "airbnb-typescript", // https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md#eslint-configs
    // "eslint:recommended",
    // "plugin:@typescript-eslint/eslint-recommended",
    // "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "prettier",
    "prettier/react",
    "prettier/@typescript-eslint",
  ],
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: "module",
    project: "./tsconfig.json",
  },
  plugins: ["react-hooks", "react", "@typescript-eslint"],
  rules: {
    "react/destructuring-assignment": ["off"], // props.*の形で使えるように
    "arrow-body-style": 0
  },
};

```

### pretteir用の設定ファイル追加

```bash
echo '{\n\t"printWidth": 120\n}' > .prettierrc
```

## Styled-components、Material-UIの導入

### インストール

```bash
yarn add @material-ui/core @material-ui/icons styled-components
yarn add -D @types/styled-components babel-plugin-styled-components
```

### 不要ファイルを削除

```bash
rm -rf pages styles/*.css
```

### ファイル作成

```bash
mkdir -p ./src/pages ./src/components && touch .babelrc ./styles/theme.ts ./src/pages/_document.tsx ./src/pages/_app.tsx ./src/pages/index.tsx ./src/components/ButtonWithBackgroundColor.tsx 
```

公式の例などを参考にファイル作成する
[公式例(Material-UI)](https://github.com/mui-org/material-ui/tree/master/examples/nextjs)
[公式例(styled-components)](https://github.com/vercel/next.js/tree/master/examples/with-styled-components)
[Material-UIとstyled componentsで，next.jsのcssをいい感じに管理する (Jest/TypeScript対応版)](https://qiita.com/o3c9/items/2551820edc156704edba)

以下の点がポイントでしょうか

- `<StylesProvider injectFirst>`を忘れない
  - https://material-ui.com/guides/interoperability/#controlling-priority-3
- `.babelrc`を作成し、`babel-plugin-styled-components`を有効化する
  - こうしないとスタイルを変更した際に`Warning: Prop 'className' did not match. Server: "***" Client: "***"`といったエラーが出ます。
- `next styled-components`等で検索すると`"plugins": [["styled-components", { "ssr": true, "displayName": true, "preprocess": false }`といった設定がよく出てきます
  - `displayName`はデバッグ用なので、`production`では`false`にした方が良さそうです。
  - `preprocess`は公式ドキュメントでは見つかりませんでしたので、省いています。
    - [[babel-plugin] preprocess breaks pseudo selectors](https://github.com/styled-components/styled-components/issues/1559)に`Preprocess is obsolete and will be removed soon`と書いているので、削除されたのでしょうか?
- `_document.tsx`での`styles`の順番をMaterial-UI, styled-componentsの順にする
  - 逆にすると、syled-componentsで上書きしたMaterial-UIのコンポーネントが一瞬表示されます
  - 下の例で言うと、`みどり`ボタンや`きいろ`ボタンが、ページ表示直後のみDefault色が表示され、色が切り替わるという流れになります。

```ts:./styles/theme.ts
import { createMuiTheme } from "@material-ui/core/styles";
import { red } from "@material-ui/core/colors";

// Create a theme instance.
const theme = createMuiTheme({
  palette: {
    primary: {
      main: "#226cd6",
    },
    secondary: {
      main: "#19857b",
    },
    error: {
      main: red.A400,
    },
    background: {
      default: "#fff",
    },
  },
});

export default theme;

```

```tsx:./src/pages/_document.tsx
/* eslint-disable react/jsx-props-no-spreading */
import { ServerStyleSheets } from "@material-ui/core/styles";
import Document, { Head, Html, Main, NextScript } from "next/document";
import React from "react";
import { ServerStyleSheet } from "styled-components";
import theme from "../../styles/theme";

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="ja-JP">
        <Head>
          {/* PWA primary color */}
          <meta name="theme-color" content={theme.palette.primary.main} />
          <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
  // Resolution order
  //
  // On the server:
  // 1. app.getInitialProps
  // 2. page.getInitialProps
  // 3. document.getInitialProps
  // 4. app.render
  // 5. page.render
  // 6. document.render
  //
  // On the server with error:
  // 1. document.getInitialProps
  // 2. app.render
  // 3. page.render
  // 4. document.render
  //
  // On the client
  // 1. app.getInitialProps
  // 2. page.getInitialProps
  // 3. app.render
  // 4. page.render

  // Render app and page and get the context of the page with collected side effects.
  const MuiSheets = new ServerStyleSheets();
  const styledComponentsSheet = new ServerStyleSheet();
  const originalRenderPage = ctx.renderPage;

  try {
    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) => styledComponentsSheet.collectStyles(MuiSheets.collect(<App {...props} />)),
      });

    const initialProps = await Document.getInitialProps(ctx);

    return {
      ...initialProps,
      // Styles fragment is rendered after the app and page rendering finish.
      styles: [
        ...React.Children.toArray(initialProps.styles),
        MuiSheets.getStyleElement(),
        styledComponentsSheet.getStyleElement(),
      ],
    };
  } finally {
    styledComponentsSheet.seal();
  }
};

```

```tsx:./src/pages/_app.tsx
/* eslint-disable react/jsx-props-no-spreading */
import CssBaseline from "@material-ui/core/CssBaseline";
import { MuiThemeProvider, StylesProvider } from "@material-ui/core/styles";
import { AppProps } from "next/app";
import React from "react";
import { ThemeProvider } from "styled-components";
import theme from "../../styles/theme";

function MyApp({ Component, pageProps }: AppProps) {
  React.useEffect(() => {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector("#jss-server-side");
    if (jssStyles && jssStyles.parentElement) {
      jssStyles.parentElement.removeChild(jssStyles);
    }
  }, []);

  return (
    <StylesProvider injectFirst>
      <MuiThemeProvider theme={theme}>
        <ThemeProvider theme={theme}>
          {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
          <CssBaseline />
          <Component {...pageProps} />
        </ThemeProvider>
      </MuiThemeProvider>
    </StylesProvider>
  );
}

export default MyApp;

```

```tsx:./src/components/ButtonWithBackgroundColor.tsx
import { Button } from "@material-ui/core";
import React from "react";
import styled from "styled-components";

const StyledButton = styled(Button)<{ background_color: string }>`
  background-color: ${(props) => props.background_color};
`;

export interface Props {
  backgroundColor: string;
  children: React.ReactNode;
}

const ButtonWithBackgroundColor: React.FC<Props> = (props: Props) => {
  return (
    <StyledButton variant="contained" background_color={props.backgroundColor}>
      {props.children}
    </StyledButton>
  );
};

export default ButtonWithBackgroundColor;

```

```tsx:./src/pages/index.tsx
import { Box, Button } from "@material-ui/core";
import React from "react";
import styled from "styled-components";
import ButtonWithBackgroundColor from "../components/ButtonWithBackgroundColor";

const RedText = styled.span`
  font-size: 50px;
  color: red;
`;

const StyledButton = styled(Button)`
  background-color: green;
`;

const Home: React.FC = () => {
  return (
    <Box>
      <Button color="primary" variant="contained">
        プライマリー
      </Button>

      <StyledButton variant="contained">みどり</StyledButton>

      <ButtonWithBackgroundColor backgroundColor="yellow">きいろ</ButtonWithBackgroundColor>

      <RedText>あか</RedText>
    </Box>
  );
};

export default Home;
```

```:.babelrc
{
  "presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true, "displayName": true }]]
}

```

## Storybookの導入

まずは、storybookの初期化をします。

自分で色々ダウンロードする方法もありますが、出来るだけ公式に乗っかっておきます。

参考：[Install Storybook](https://storybook.js.org/docs/react/get-started/install)

```bash
npx sb init
```

### `preview.js`の編集

themeを作っており、Material-UI, styled-componentsを使用しているので、それらを反映するために、decoratorを作成する必要があります。

参考：https://storybook.js.org/docs/react/essentials/toolbars-and-globals#create-a-decorator

```js:.storybook/preview.js
import CssBaseline from "@material-ui/core/CssBaseline";
import { MuiThemeProvider, StylesProvider } from "@material-ui/core/styles";
import React from "react";
import { ThemeProvider } from "styled-components";
import theme from "../styles/theme";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
};

const withThemeProvider = (Story, context) => {
  return (
    <StylesProvider injectFirst>
      <MuiThemeProvider theme={theme}>
        <ThemeProvider theme={theme}>
          {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
          <CssBaseline />
          <Story {...context} />
        </ThemeProvider>
      </MuiThemeProvider>
    </StylesProvider>
  );
};
export const decorators = [withThemeProvider];
```

### storiesの追加

```tsx:./src/stories/components/ButtonWithBackgroundColor.stories.tsx
import { Meta, Story } from "@storybook/react";
import React from "react";
import ButtonWithBackgroundColor, { Props } from "../../components/ButtonWithBackgroundColor";

export default {
  title: "Parts/Button",
  component: ButtonWithBackgroundColor,
  argTypes: {
    backgroundColor: { control: "color" },
  },
} as Meta;

const Template: Story<Props> = (args) => <ButtonWithBackgroundColor {...args} />;

export const Yellow = Template.bind({});
Yellow.args = {
  backgroundColor: "yellow",
  children: "きいろ",
};

export const Green = Template.bind({});
Green.args = {
  backgroundColor: "green",
  children: "みどり",
};

export const Default = Template.bind({});
Default.args = {
  children: "デフォルト",
};
```

```tsx:./src/stories/pages/index.stories.tsx
import { Story } from "@storybook/react";
import React from "react";
import Home from "../../pages/index";

export default {
  title: "Pages/Home",
  component: Home,
};

const Template: Story = () => <Home />;

export const Primary = Template.bind({});
```

### 確認

```
yarn storybook
```

## デプロイ

公式ドキュメントの通りに、Vercelにデプロイします。
最初はfirebase Hostingを使おうかと思っていたのですが、色々調べた結果、純正使っておくのがベターそうだなとなりました。

公式ドキュメント：https://nextjs.org/learn/basics/deploying-nextjs-app

簡単な作業を数回するだけでデプロイすることができました👏

プルリクを作成すればプレビューサイトも作成してくれるみたいですね、感動しました。

## おわりに

以上で、Next.js + Material-UI + styled-components + TypeScript + Storybook + ESLint + prettier + Vercel の環境を作成することができました👏

型ありプレビューありでデプロイも簡単にできるので、爆速で開発していけそうですね💪

## 参考

[Next.jsプロジェクトでMaterial UIを使う方法](https://tech.playground.style/javascript/next-material-ui/)
[Next.jsでclassNameが見つからなくなるバグの対処方](https://qiita.com/hiropanda/items/db4e10d826e3ad9a9b33)
[Server-side rendered styled-components with Nextjs](https://jsramblings.com/server-side-rendered-styled-components-with-nextjs/)
[[babel-plugin] preprocess breaks pseudo selectors](https://github.com/styled-components/styled-components/issues/1559)
[Next.js 4年目の知見：SSRはもう古い、VercelにAPIサーバを置くな](https://qiita.com/jagaapple/items/faf125e28f8c2860269c)
[Next.js周りのボイラープレート(Typescript + Jest + Storybook + ESLint + Prettier + styled-components)](https://qiita.com/daku10/items/c00d087458f2f1de6545)
[Next.js + TypeScriptのプロジェクトにStorybookを導入する](https://qiita.com/keitakn/items/982d7e6cdfc294c82a95)
[Next.js+TypeScript 環境で Storybook を使う](https://blog.gaji.jp/2020/06/19/4210/)