デザインセンスがないので、フロントの開発では趣味でも仕事でもCSSフレームワークを使うことが多いです。
当サイトもMaterial-UIというCSSフレームワークを使って構築しています。
カスタマイズで試行錯誤したところがあったので、備忘録も兼ねてGatsbyでMaterial-UIをどのように使っているか説明します。
Material-UI導入
まずはMaterial-UIをインストールします。
npm install @material-ui/core# Material Iconを使う場合はこちらもnpm install @material-ui/icons
続いてGatsbyでMaterial-UIを扱いやすくするプラグインを入れます。
npm install gatsby-plugin-material-ui
FOUCの解決、ベンダープレフィックスの追加、CSSの最小化をしてくれます。
インストールしたらgatsby-config.js
のプラグインに追加します。
plugins: ['gatsby-plugin-material-ui']
これでGatsbyでMaterial-UIを使えるようになりました!
テーマをサイト全体に適用する
Material-UIにはテーマという仕組みがあり、コンポーネントの色や文字の大きさなどを一元管理してカスタマイズできます。
カスタマイズしたテーマをGatsbyで生成するページすべてに適用するため、Gatsby Browser APIsのwrapRootElement
というAPIを使います。
まずはMaterial-UIのテーマを適用するコンポーネントを作ります。
import { createMuiTheme, CssBaseline } from '@material-ui/core'import { ThemeProvider } from '@material-ui/styles'import React from 'react'const MuiThemeProvider: React.FC = ({ children }) => {const theme = createMuiTheme({palette: {primary: {main: '#fff',},},})return (<ThemeProvider theme={theme}><CssBaseline />{children}</ThemeProvider>)}export default MuiThemeProvider
CssBaseline
はnormalize.css
のようなコンポーネントで、Material-UIを使うにあたって適当なデフォルトCSSを提供してくれます。
gatsby-browser.js
でこのコンポーネント使ってルートをラップします。
import { WrapRootElementBrowserArgs } from 'gatsby'import React, { ReactNode } from 'react'import MuiThemeProvider from '../components/mui-theme-provider'export const wrapRootElement = ({element,}: WrapRootElementBrowserArgs): ReactNode => {return <MuiThemeProvider>{element}</MuiThemeProvider>}
これですべてのページでテーマが適用されるようになりました!
各コンポーネントで以下のようにテーマの値を取り出すことができます。
import { makeStyles, useTheme } from '@material-ui/core'const theme = useTheme()// mui-theme-provider.tsxで定義したthemeが得られる// theme.palette.primary.main => '#fff'const useStyles = makeStyles(theme => {// mui-theme-provider.tsxで定義したthemeが得られる// theme.palette.primary.main => '#fff'})
個人的はまりポイント
最初は各ページで利用しているlayout.tsx
コンポーネントのなかでテーマを設定しようとしてました。(テーマと関係ないコードは省略しています)
const Layout: React.FC<Props> = ({ title, children }) => {const theme = useTheme()// theme.palette.primary.main => '#3f51b5' default value!!!return (<ThemeProvider><CssBaseline /><AppBar position="static" elevation={1}>// 省略</AppBar><main>{children}</main><footer>// 省略</footer></ThemeProvider>)}export default Layout
const BlogIndex: React.FC<PageProps<BlogIndexQuery>> = ({ data }) => {const siteTitle = data.site?.siteMetadata?.title || 'Title'const posts = data.allContentfulBlogPost.nodesconst theme = useTheme()// theme.palette.primary.main => '#3f51b5' default value!!!return (<Layout title={siteTitle}><SEO /><ArticleList title="All Posts" posts={posts} /></Layout>)}
これだとテーマは正しく適用されません。
layout.tsx
やindex.tsx
が、ThemeProvider
の子になっていないからです。
ThemeProvider
はテーマを適用したいコンポーネントの親である必要があります。
上記のコードだとindex.tsx
がルートのコンポーネントであり、その中でThemeProvider
を呼び出している形になります。
ページ自体にテーマを適用するにはwrapRootElement
を使ってindex.tsx
を呼び出すよりも上のコンポーネントとして定義する必要がありました。
ダークモードを実装
Material-UIにはダークモードがあらかじめ用意されていて、palette.type
に'dark'を設定するだけでダークモードになります。
const darkTheme = createMuiTheme({palette: {type: 'dark',},})
ダークモードの切り替えを実装する
今回はダークモードを切り替えできるように実装します。
stateでダークモードの状態を保持し、リロードした際にも引き継がれるように、Local Storageに保存しておきます。
まず、palette.type
の値を保持するstateを作成します。
const [paletteType, setPaletteType] = useState<string | null>(null)
次にlocalStorage
からpaletteType
を読み出します。
localStorage
はレンダー後でないと変数が取得できないので注意が必要です。
useEffect(() => {setPaletteType(localStorage.getItem('paletteType'))})
paletteType
の値を'light'/'dark'で切り替える関数を定義します。切り替えたタイミングでLocal Storageにも保存しています。
const togglePaletteType = () => {const newPaletteType = paletteType === 'light' ? 'dark' : 'light'setPaletteType(newPaletteType)localStorage.setItem('paletteType', newPaletteType)}
paletteType
とtogglePaletteType
を共有するため、コンテクストを使用します。
これによって好きなコンポーネントでダークモードの切り替えが行えるようになります。
export const paletteTypeContext = createContext<[string | null, (() => void) | undefined]>([null, undefined])
作成したコンテクストのプロバイダを仕込みます。
<ThemeProvider theme={theme}><CssBaseline /><paletteTypeContext.Provider value={[paletteType, togglePaletteType]}>{children}</paletteTypeContext.Provider></ThemeProvider>
以上で、mui-theme-provider.tsx
の修正は完了です。
完成コード
import { createMuiTheme, CssBaseline } from '@material-ui/core'import { grey } from '@material-ui/core/colors'import { ThemeProvider } from '@material-ui/styles'import React, { createContext, useEffect, useMemo, useState } from 'react'export const paletteTypeContext = createContext<[string | null, (() => void) | undefined]>([null, undefined])const MuiThemeProvider: React.FC = ({ children }) => {const [paletteType, setPaletteType] = useState<string | null>(null)useEffect(() => {setPaletteType(localStorage.getItem('paletteType'))})const togglePaletteType = () => {const newPaletteType = paletteType === 'light' ? 'dark' : 'light'setPaletteType(newPaletteType)localStorage.setItem('paletteType', newPaletteType)}const theme = useMemo(() =>createMuiTheme({palette: {type: paletteType === 'dark' ? 'dark' : undefined,},}),[paletteType])return (<ThemeProvider theme={theme}><CssBaseline /><paletteTypeContext.Provider value={[paletteType, togglePaletteType]}>{children}</paletteTypeContext.Provider></ThemeProvider>)}export default MuiThemeProvider
切り替えボタンを実装する
useContext
で上記で実装したpaletteType
とtogglePaletteType
が使用できるようになります。
import React, { useContext } from 'react'import { paletteTypeContext } from './mui-theme-provider'const [paletteType, togglePaletteType] = useContext(paletteTypeContext)
今回はこれをヘッダーに仕込みました。
const Layout: React.FC<Props> = ({ title, children }) => {const [paletteType, togglePaletteType] = useContext(paletteTypeContext)const classes = useStyle()return (<React.Fragment><AppBar position="static" elevation={1}><Toolbar variant="dense">// 省略<IconButtononClick={togglePaletteType}color="inherit"aria-label="toggle theme">{paletteType === 'dark' ? <Brightness4 /> : <BrightnessHigh />}</IconButton></Toolbar></AppBar><main>{children}</main><footer>// 省略</footer></React.Fragment>)}export default Layout
まとめ
Material-UIのテーマをすべてのページに適用するため、wrapRootElement
という仕組みを使いました。また、コンテクストを使うことでテーマを切り替えられるようにしています。
あまり使ったことがなかったコンテクストやGatsbyの機能の知見が得られて勉強になりました。 ReactやGatsbtの経験はまだまだなので、「こうしたほうがいい」とか「これはおかしい」という点があれば気軽にコメントください!