ReactやVue、Angularが普及した現代のフロントエンド開発において、NextやNuxtのようなフレームワーク(パッケージ)は欠かせないと言えるでしょう。単にSSRやSSGだけでなく、さまざまな最適化を任せることができます。一方で、
- ボタンやナビゲーションバーなどをスクラッチで実装することなく、既存のコンポーネントを使用したい。
- スタイルをコンポーネントファイルごとに管理したい。
などの動機から、スタイルを管理するフレームワークを使用するディベロッパーも少なくありません。そこで、本稿ではReact / Next.jsにフォーカスし、Material-UIを使ってスタイル管理を行うことを目指します。もちろん、実装にはTypeScriptを使用します。
目次
0. 環境
- macOS Big Sur 11.5.2
- MacBook Pro (13-inch, M1, 2020)
- Node.js v16.9.1
- WebStorm 2021.1
1. 技術スタックの概説
1.1. React
Reactは主にFacebook社が開発しているUI構築フレームワークで、コンポーネントの管理や宣言的な記述を可能にします。同様のものにVueやAngularがあります。Google Trendsによると、2020年2月16日以降ReactはVueとAngularより多く検索されています。また、地域別に見ると、アメリカやイギリス、カナダ、ロシアで最も多く検索されています。
1.2. Next.js
Next.jsは主にVercel社が開発している、Reactをエンパワーするフレームワークです。サーバーサイドレンダリング(SSR)や静的サイトジェネレーション(SSG)はもちろん、画像の最適化やバンドル、SEOなどさまざまなことを勝手に行ってくれます。
1.3. TypeScript
TypeScriptは主にMicrosoft社が開発している、ディベロッパーエクスペリエンスを改善するためのJavaScriptのスーパーセットです。JavaScriptに「型」を付与することで、本来は実行するまで分からないエラーをコンパイル時に(ほとんどはタイプした瞬間に)検出するようにします。モダンなフロントエンド開発にはほとんど欠かせないと言って良いでしょう。
1.4. Material-UI
Material-UIはコミュニティが開発している、スタイリングのためのフレームワークです。大きく分けて3つのタスクを追います。
- Material DesignベースのReactコンポーネントを提供します。BootstrapのようなCSSフレームワークと同じ役割と考えて差し支えないと思います。
- コンポーネントのスタイリングを行います。styled-componentsやCSS Modules、Tailwind CSSとおおよそ同じような役割と考えて差し支えないと思います。
- 全体のテーマを管理します。例えば、ダークモードへのスイッチを簡単にします。
Material-UIに類するフレームワークのどれを使うべきかは、それぞれ一長一短であり、半ば宗教的な議論に終着してしまいがちですが、Material-UIを使うメリットとして
- ゼロから実装する必要はなく、実装の手間が省ける。
- コンポーネント横断的なスタイル(色など)を管理しやすい。
- コンポーネントファイル(.jsxや.tsxファイル)内では、コンテンツとスタイルを分離できる。
などが挙げられます。
1.5. Vercel
VercelはVercel社が提供する、Next.jsアプリのデプロイ先の一つです。SSGやCDNへの配置などを一手に引き受けてくれます。デプロイだけでなく、プルリクエストに紐づいたプレビューなどを驚くほど簡単に実現できます。個人開発用には無料で使えます。(ただし、組織ではなく個人のリポジトリに紐づける必要があります。)今回はデプロイについての説明は省略しますが、Next.jsアプリ開発で使いたい技術スタックの一つです。
2. 実装例
2.1. Next.jsプロジェクトを立ち上げる
Node.jsがインストールされているかを確認します。
% node -v
v16.9.1
TypeScriptをサポートしたnextプロジェクトを立ち上げます。npmであれば
% npx create-next-app --ts
Yarnであれば
% yarn create next-app --typescript
でインストールできます。(以下、簡単のためnpmのコマンドのみを記載します。)プロジェクト名など必要事項を適宜記入してください。確認のため、アプリを立ち上げてみましょう。
% cd path/to/project
% npm run dev
ブラウザで以下のリンクを立ち上げてみましょう。
開発用サーバは Ctrl+C
で閉じられます。
ついでにですが、./src
ディレクトリを作成し、./pages
を ./src
配下に移動させておきましょう。
2.2. Material-UIをインストールする
2.2.1. npmでインストールする
npmからMaterial-UIをインストールしましょう。Material-UIは主なものに限ると3つのパッケージを提供しています。
-
@material-ui/core
- 最も一般的なパッケージです。各種コンポーネントやスタイリングのための関数などが提供されています。
-
@material-ui/icons
- Material Designのアイコンを使用できます。こちらから使用できるアイコンを検索できます。
-
@material-ui/lab
-
core
に移動する前のアルファ版的なコンポーネントが提供されています。
-
必要となるパッケージを選んでください。本項ではすべてインストールしようと思います。
% npm i -S @material-ui/core @material-ui/icons @material-ui/lab
./package.json
を確認すると、"dependencies"
に追加されていることが分かります。
2.3. SSRに対応させる
このままだとスタイル定義のタイミングにずれが生じ、Warning: Prop `className` did not match.
のような注意がなされます。これを解消するために、nextアプリにおける2つの特別なファイル、すなわち、./src/pages/_app.tsx
と ./src/pages/_document.tsx
を編集します。これらのファイルの概説は以下の通りです。
-
_app.tsx
- 各ページに共通する部分を記述するためのものです。
-
_document.tsx
-
<html>
や<body>
をカスタマイズするためのものです。
-
これらを以下のように編集してください。
import React, { useEffect } from 'react'
import type { AppProps } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side')
jssStyles?.parentElement?.removeChild(jssStyles)
}, [])
return (
<Component {...pageProps} />
)
}
import React from 'react'
import Document, { Head, Html, Main, NextScript } from 'next/document'
import { ServerStyleSheets } from '@material-ui/core/styles'
class MyDocument extends Document {
render() {
return (
<Html>
<Head>
</Head>
<body>
<Main/>
<NextScript/>
</body>
</Html>
)
}
}
MyDocument.getInitialProps = async (ctx) => {
const sheets = new ServerStyleSheets()
const originalRenderPage = ctx.renderPage
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
}
}
export default MyDocument
2.4. テスト
適当にボタンを配置して、Material-UIが適切に使えるかどうかをテストしてみましょう。その前にまず、今回はCSS Modulesを使用しないので、デフォルトの ./styles
ディレクトリを削除します。次に ./src/pages/index.tsx
を次のように書き換えます。
import React from 'react'
import Button from '@material-ui/core/Button'
export default function Home() {
return (
<Button variant="contained">Success!</Button>
)
}
単にSuccess!という表示のボタンを配置しました。最後に、これを確認するために npm run dev
コマンドで開発用サーバを起動し、ブラウザで http://localhost:3000 にアクセスします。その結果、ボタンが表示されるとOKです。念のため、開発者用ツールからコンソールを確認し、Warningが出ていないこともチェックしておいてください。
3. 運用例
ここまでで、Next.js上でMaterial-UIを使用する準備ができました。本章では、実際にコーディングする際に、どのようにMaterial-UIを使うべきかの一例を示します。
3.1. React Componentについて
まずいきなりですが、コンポーネントファイルの一例を見ていきましょう。
import React from 'react'
import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles'
import { createStyles, Theme } from '@material-ui/core/styles'
import { Typography } from '@material-ui/core'
interface TitleProps extends WithStyles<typeof styles> {}
const styles = (theme: Theme): StyleRules => createStyles({
root: {
'& .MuiTypography-subtitle1': {
fontSize: '1.5em',
fontWeight: 300
},
'& .MuiTypography-h5': {
fontWeight: 200,
},
},
wrapper: {
backgroundImage: 'linear-gradient(to right, #000428BB, #004e92BB), url(\'/path/to/image\')',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
position: 'relative',
margin: 0,
height: '95vh',
minHeight: '450px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
title: {
padding: '0 20%',
textAlign: 'center',
color: theme.palette.common.white,
[theme.breakpoints.down('xs')]: {
padding: '0 5%',
},
},
})
const TitleSection: React.FC<TitleProps> = ({classes}) => {
return (
<section className={classes.root}>
<div className={classes.wrapper}>
<div className={classes.title}>
<Typography variant="h2">
<strong>Title</strong>
</Typography>
</div>
</div>
</section>
)
}
export default withStyles(styles, { withTheme: true })(TitleSection)
これをもとに詳しく見ていきましょう。
3.1.1. React Componentの型
React Componentの定義式では通常通り、React.FC<FooBarProps>
を使用します。FooBarProps
は WithStyles<typeof styles>
を拡張したインタフェースで、プロパティを自由に定義できます。(名前はパスカルケースである限り任意です。)最終的には withStyles
でこのReact Componentをラップしたものをエクスポートします。こうすることで、次項3.1.2で説明するスタイリングが有効となります。
3.1.2. React Componentのスタイリング
スタイリングは基本的に styles
が担います。styles
は Theme
型(3.2.3で詳しく説明)を引数にとり、StyleRules
型を返す関数です。上の例では createStyles
関数をそのまま返しています。createStyles
関数はCSSライクなオブジェクトを引数にとります。つまり、この関数の引数に各種スタイルを定義していきます。このスタイリングは、コンポーネント内で閉じられており、子コンポーネント以外の別のコンポーネントのスタイルに影響することはありません。
プロパティ名はケバブケースではなく、キャメルケースで記述してください。また、プロパティ名を '& .MuiFooBar'
などとすることでオリジナルのMaterial-UIコンポーネントを上書きすることができます。さらには、[theme.breakpoints.down('xs')]
などとすることでブレイクポイントごとのスタイルの変更、すなわち、レスポンシブデザインを可能になります。
定義するだけではスタイルは適用されません。具体的には以下の2つが必要です。
- 前項で説明した通り、
withStyles
関数でReact Componentをラップする必要があります。 - スタイルを適用したい要素に
className
を通じてスタイルを与えます。このとき、createStyles
に渡したオブジェクトがReact Componentのprops.classes
に格納されています。
3.1.3. スタイルに props
の内容を反映させたい場合
makeStyles
関数を用います。React Componentの外で、
const useStyles = makeStyles({
foo: (props: FooProps) => ({
bar: 'baz',
}),
})
のように useStyles
フックを定義します。この中では props
を使用することができ、スタイルに props
の内容を反映できるようになります。あとはReact Component内で
const SomeComponent: React.FC<SomeProps> = ({classes, ...props}) => {
const classesWithProps = useStyles(props)
return (
...
)
}
のように呼び出せば、前項3.1.2.の classes
と同じように、classesWithProps
を使用できます。
3.2. 全体のスタイルとテーマについて
3.2.1. テーマ管理
色やフォントファミリー、ブレイクポイントやスペースなどのテーマを管理することができます。これを用いるとダークモードなどを容易に実装できます。./src/styles
に次のようなファイルを追加します。
import { createTheme, responsiveFontSizes } from '@material-ui/core'
/* COLOR */
const black = '#343a40'
const white = '#F4F4F4'
const background = '#F4F4F4'
/* BREAKPOINTS */
const xl = 1920
const lg = 1280
const md = 960
const sm = 600
const xs = 0
/* SPACING */
const spacing = 8
/* FONT FAMILY */
const fontFamily = [
'Poppins',
'"Helvetica Neue"',
'Arial',
'Noto Sans JP',
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
]
const theme = createTheme({
palette: {
primary: {
main: '#69D2E7',
light: '#B1E2EC',
dark: '#51A4B5',
},
secondary: {
main: '#FA6900',
light: '#FDAD0D',
dark: '#E3460B',
},
common: {
black: '#343a40',
white: '#F4F4F4',
},
info: {
main: '#005CAF',
light: '#0087FC',
dark: '#00437D',
},
success: {
main: '#00AA90',
light: '#00F7D2',
dark: '#007866',
},
warning: {
main: '#FFB11B',
light: '#FFDB0F',
dark: '#E8820E',
},
error: {
main: '#CB1B45',
light: '#D55B78',
dark: '#991433',
},
tonalOffset: 0.2,
background: {
default: background,
},
},
spacing: spacing,
breakpoints: {
values: {
xl,
lg,
md,
sm,
xs
}
},
typography: {
fontFamily: fontFamily.join(','),
},
overrides: {
MuiToolbar: {
root: {
justifyContent: 'space-between'
}
},
},
})
export default responsiveFontSizes(theme)
メインカラー、サブカラーの定義などの他に、Material-UIコンポーネントのグローバルな上書き(カスタマイズ)もできます。
これを使用するには
-
_app.tsx
内でMuiThemeProvider
を用いてテーマを提供する必要があります。- 渡すテーマを入れ替えれば、テーマが変更できます。
- 各コンポーネントファイルでコンポーネントのラッピングの際に
withStyles(styles, { withTheme: true })
のようにオプション引数を与える必要があります。
3.2.2. グローバルスタイル
グローバルに適用したいスタイルにはスタイリングの際に @global
プロパティを与えます。新たに GlobalStyles
というコンポーネントを作り、それを _app.tsx
で呼び出すという方法を私は採用することが多いです。
import { withStyles } from "@material-ui/core";
import { Theme } from "@material-ui/core/styles";
type Styles = (theme: Theme) => object
const styles: Styles = theme => ({
'@global': {
'html': {
padding: 0,
margin: 0,
scrollBehavior: 'smooth',
},
'body': {
padding: 0,
margin: 0,
},
'a': {
color: 'inherit',
textDecoration: 'none',
},
'*': {
boxSizing: 'border-box'
},
'.container': {
width: "100%",
height: "100%",
paddingRight: theme.spacing(4),
paddingLeft: theme.spacing(4),
margin: "auto",
[theme.breakpoints.up("sm")]: {
maxWidth: 540
},
[theme.breakpoints.up("md")]: {
maxWidth: 720
},
[theme.breakpoints.up("lg")]: {
maxWidth: 1170
}
},
}
})
function GlobalStyles() {
return null;
}
export default withStyles(styles, { withTheme: true })(GlobalStyles);
import React, { useEffect } from 'react'
import Head from 'next/head'
import { AppProps } from 'next/app'
import theme from '../styles/theme'
import CssBaseline from '@material-ui/core/CssBaseline'
import GlobalStyles from '../styles/GlobalStyles'
import { MuiThemeProvider } from '@material-ui/core/styles'
export default function PutchApp(props: AppProps) {
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side')
jssStyles?.parentElement?.removeChild(jssStyles)
}, [])
const {Component, pageProps} = props
return (
<>
<Head>
<title>Title</title>
<meta charSet="utf-8"/>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
</Head>
<MuiThemeProvider theme={theme}>
<CssBaseline/>
<GlobalStyles/>
<Component {...pageProps} />
</MuiThemeProvider>
</>
)
}
<CssBaseLine/>
はCSSをリセットするものです。
4. 最後に
ここまでNext.jsアプリでMaterial-UIを使う方法を説明してきました。Material-UIは単にMaterial Designのコンポーネントを提供するだけでなく、テーマやスタイル全体を管理してくれる非常に強力なものです。ぜひ一度、使用を検討してみてはいかがでしょうか。この記事良かったなと思った方は、ぜひLGTMをお願いします。(記事投稿のモチベーションとなります。)
5. 参考
5.1. Next.jsについて
公式ドキュメント
5.2. Material-UIについて
公式ドキュメント
MUI Treasury