🎨

next.js 14 App Router + MUI dark/light モード切り替え実装 server/client コンポーネント

2024/01/15に公開

ThemeContext.tsxを作る

dark/light モード切り替えのためのそれぞれのpaletteをlightPaletteとdarkPaletteに定義し、その他のtheme定義はthemeOptionsで定義した前提で

// src/contexts/ThemeContext.tsx
import { createContext, useMemo, useState } from 'react'
import { createTheme } from '@mui/material/styles'
import {
  themeOptions,
  lightPalette,
  darkPalette,
} from '@/assets/styles/customTheme'

export const ColorModeContext = createContext({
  toggleColorMode: () => {},
})

export function useThemeContent() {
  const [mode, setMode] = useState<'light' | 'dark'>('dark')

  const colorMode = {
    toggleColorMode: () => {
      setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'))
    },
  }

  const themePalette = mode === 'dark' ? darkPalette : lightPalette
  const theme = useMemo(
    () => createTheme({ palette: { mode, ...themePalette }, ...themeOptions }),
    [mode]
  )

  return { colorMode, theme }
}

layout.tsxのなかでContext.Providerでラップする

// src/app/(application)/app/layout.tsx
'use client'
import { ThemeProvider } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
import { ColorModeContext, useThemeContent } from '@/contexts/ThemeContext'

export default function AppLayout({ children }: { children: React.ReactNode }) {
  const { colorMode, theme } = useThemeContent()

  return (
    <ColorModeContext.Provider value={colorMode}>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        {children}
      </ThemeProvider>
    </ColorModeContext.Provider>
  )
}

ラッパーされるコンポーネント内でテーマを切り替え

// src/app/(application)/app/_lib/test-toggle.tsx
import { useContext } from 'react'

import { useTheme } from '@mui/material'
import IconButton from '@mui/material/IconButton'
import Brightness4Icon from '@mui/icons-material/Brightness4'
import Brightness7Icon from '@mui/icons-material/Brightness7'

import { ColorModeContext } from '@/contexts/ThemeContext'

export default function ToggleTheme() {
  const colorMode = useContext(ColorModeContext)
  const theme = useTheme()

  return (
    <IconButton
      sx={{ ml: 1 }}
      onClick={colorMode.toggleColorMode}
      color='inherit'
    >
      {theme.palette.mode === 'dark' ? (
        <Brightness7Icon />
      ) : (
        <Brightness4Icon />
      )}
    </IconButton>
  )
}

最初はuseThemeContentからthemeをインポートしてひたすら動かない(新しいthemeになっちゃったっぽい)
→ themeはmuiからインポートしたやつを使う

import { useTheme } from '@mui/material'

export default function HogeComponent() {
  const theme = useTheme()
  return (
    <div>
      {JSON.stringify(theme.palette)}
    </div>
  )
}

layout.tsxとpage.tsxのサーバーサイド・クライアントサイド問題?

theme切り替えのuseContextがlayout.tsxに入ってるため、layout.tsxはクライアントサイド
そうするとpage.tsxはcookiesなどからユーザー情報取得したりするのでpage.tsxはサーバサイド
イメージとしてlayout.tsxはpage.tsxの親のようなもので、layout.tsxに'use client'しているのに、page.tsx内のcookies処理はなぜ怒られないのかよくわからなかったが...とりあえずlayoutがクライアントサイドでpageがサーバーサイドで落ち着きそう

Discussion