Next.js x TypeScript x styled-components x Material-UI x Apollo で開発を始める

Next.js に複数の主要技術を組み合わせた時用まとめです。

賞味期限短めです。

動機

いずれの技術もReactで開発する時によく採用されますが、それら全てを同時に採用した場合の情報があまり見つからなかったので、まとめました。

免責事項

パフォーマンスも含め、本番環境で問題なく動作するところまでは確認しておりませんので、あしからず。

Next.js

困ったらまずこちら:Next.js 公式ドキュメント

TypeScript

TypeScript はもはや必須。

公式が提供する TypeScript と styled-components 入りのリポジトリを使用する

公式サンプル:TypeScript & Styled Components Next.js example

yarn create next-app --example with-typescript name-your-app

tsconfig.json を修正する

# tsconfig.jsonが追加されていない場合
touch tsconfig.json

tsconfg.json のコンパイラーオプションについて:Compiler Options(公式ドキュメント)

以下例です。

tsconfig.json

{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"moduleResolution": "node",
"isolatedModules": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"allowJs": true,
"checkJs": true,
"noEmit": true,
"sourceMap": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

styled-components x Material-UI

yarn add @material-ui/core
# styled-componentsが追加されていない場合
yarn add styled-components
yarn add --dev @types/styled-components

Babel ファイルを作成・修正する

理由:SSR サポート・デバッグサポートを追加するため(公式ドキュメント

# babel-plugin-styled-componentsが追加されていない場合
yarn add --dev babel-plugin-styled-components

.babelrc

{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true, "displayName": true, "preprocess": false }]]
}

_document.tsx を修正する

理由:styled-components と Material-UI が SSR 時にも適切にアプリケーションをラップできるようにするため(公式ドキュメント

TypeScript と styled-components が入った公式サンプル:TypeScript & Styled Components Next.js example

Material-UI が提供する公式サンプル:Next.js with TypeScript example

Material-UI の SSR についての公式ドキュメント:Server Rendering

styled-components と Material-UI の設定を同時に行う場合の参考記事:SSR with Next.js, styled-components and Material UI

_document.tsx

import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet as StyledComponentSheets } from 'styled-components';
import { ServerStyleSheets as MaterialUiServerStyleSheets } from '@material-ui/styles';
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const styledComponentSheets = new StyledComponentSheets();
const materialUiServerStyleSheets = new MaterialUiServerStyleSheets();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props =>
styledComponentSheets.collectStyles(
materialUiServerStyleSheets.collect(<App {...props} />)
)
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{styledComponentSheets.getStyleElement()}
{materialUiServerStyleSheets.getStyleElement()}
</>
)
};
} finally {
styledComponentSheets.seal();
}
}
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

_app.tsx を修正する

Material-UI が提供する公式サンプル(再掲):Next.js with TypeScript example

Material-UI の SSR についての公式ドキュメント(再掲):Server Rendering

import React, { useEffect } from 'react';
import { AppProps } from 'next/app';
import { ThemeProvider as StyledComponentsThemeProvider } from 'styled-components';
import { ThemeProvider as MaterialUIThemeProvider } from '@material-ui/core/styles';
import { StylesProvider } from '@material-ui/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import '../styles/global.scss';
const theme: Theme = createMyTheme({
palette: {
primary: {
main: '#556cd6'
},
secondary: {
main: '#19857b'
},
error: {
main: red.A400
},
background: {
default: '#fff'
}
}
});
const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
// Remove the server-side injected CSS.(https://material-ui.com/guides/server-rendering/)
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles && jssStyles.parentNode) {
jssStyles.parentNode.removeChild(jssStyles);
}
}, []);
return (
<StylesProvider injectFirst>
<MaterialUIThemeProvider theme={theme}>
<StyledComponentsThemeProvider theme={theme}>
<CssBaseline />
<Component {...pageProps} />
</StyledComponentsThemeProvider>
</MaterialUIThemeProvider>
</StylesProvider>
);
};
export default MyApp;

Apollo Client

2020年4月1日現在、新APIが追加されたnextのv9.3がリリースされて間もない、かつ@apollo/clientのv3がbeta版かつ、@apollo/react-ssr のv4がbeta版ということもあり、breaking changesを含むリリースが非常に活発なため、落ち着いたら追記・修正します。

一応私の手元では、公式サンプルを参考に、withApollo(HOC)をTypeScriptで作成して、Apollo関連パッケージを下記のバージョンのパッケージにまとめ直したら、ひとまず(データ取得からレンダリングまでは)動きました。

{
"@apollo/client": "3.0.0-beta.41",
"@apollo/react-ssr": "@4.0.0-beta.1"
}

ただ、レンダリングがかなり遅いのと、ApolloClientのインスタンス生成時に渡す認証用のトークンをlocalStorage経由で渡す以外の実装方法がわからなくて困りました。

実際、公式リポジトリのディスカッションで、上記公式サンプルの実装方法はレンダリング時のパフォーマンスが著しく悪いので推奨しないとメンテナーの方がおっしゃっています。メンテナーの方が推奨する方法を使えば、パフォーマンスの問題は解決しそうです(まだ試していません)。

*追記

色々調べていたところ、下記の素晴らしい記事を見つけました。10000いいねあげたいです。

Real World GraphQL on Next.js SSR

ESLint / Prettier(おまけ)

# ESLintとESLintプラグインとPrettier
yarn add --dev eslint prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks

設定ファイルを作成・修正する

以下例

.eslintrc

{
"extends": [
"eslint:recommended",
"plugin:jsx-a11y/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint" // prettier関連設定はextendsの最後に記述する(https://github.com/prettier/eslint-config-prettier#installation)
],
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"parser": "@typescript-eslint/parser",
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"parserOptions": {
"sourceType": "module"
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"jsx-a11y/anchor-is-valid": "off",
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}

.prettierrc

{
"arrowParens": "always",
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
}

.editorconfig

root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

NPM scripts を追加する

{
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx src/",
"lint-fix": "eslint --fix --ext .js,.jsx,.ts,.tsx src/"
}
}

結論

開発環境作るだけで一苦労。お疲れ様でした。