19
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-22

こちらをご覧ください

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

はじめに

以前React 開発時にやることという記事を書いたのですが、
今回 Next.js を使ったので、同じように最初にやるべきことを記録しておきます。

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

この記事を実行すると出来上がるもの

元となるコードを生成

yarn create next-app app

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

cd app

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

tsconfig.json等の自動更新

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

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

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

Lint & Formatter の追加

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

eslintの初期化

./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を修正

.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用の設定ファイル追加

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

Styled-components、Material-UIの導入

インストール

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

不要ファイルを削除

rm -rf pages styles/*.css

ファイル作成

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)
公式例(styled-components)
Material-UIとstyled componentsで,next.jsのcssをいい感じに管理する (Jest/TypeScript対応版)

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

  • <StylesProvider injectFirst>を忘れない
  • .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は公式ドキュメントでは見つかりませんでしたので、省いています。
  • _document.tsxでのstylesの順番をMaterial-UI, styled-componentsの順にする
    • 逆にすると、syled-componentsで上書きしたMaterial-UIのコンポーネントが一瞬表示されます
    • 下の例で言うと、みどりボタンやきいろボタンが、ページ表示直後のみDefault色が表示され、色が切り替わるという流れになります。
./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;

./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();
  }
};

./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;

./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;

./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

npx sb init

preview.jsの編集

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

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

.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の追加

./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: "デフォルト",
};
./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を使う方法
Next.jsでclassNameが見つからなくなるバグの対処方
Server-side rendered styled-components with Nextjs
[babel-plugin] preprocess breaks pseudo selectors
Next.js 4年目の知見:SSRはもう古い、VercelにAPIサーバを置くな
Next.js周りのボイラープレート(Typescript + Jest + Storybook + ESLint + Prettier + styled-components)
Next.js + TypeScriptのプロジェクトにStorybookを導入する
Next.js+TypeScript 環境で Storybook を使う

19
28
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?