📑
MUIのuseMediaQueryをNext.js(v14 App Router)で動作確認
Next.js integration対応を行なった上でuseMediaQueryを試していく。
ゴール
準備
Next.js & MUIの基本的なセットアップ
src/app/layout.tsx
import { CssBaseline, ThemeProvider } from '@mui/material';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
import { theme } from './styles/theme';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</head>
<body>
<CssBaseline />
<ThemeProvider theme={theme}>
<AppRouterCacheProvider>{children}</AppRouterCacheProvider>
</ThemeProvider>
</body>
</html>
);
}
src/styles/theme.ts
'use client';
import { createTheme } from '@mui/material';
export const theme = createTheme();
src/app/page.tsx
import { Box } from '@mui/material';
import { SampleComponent } from './SampleComponent';
export default function Home() {
return (
<Box component="main">
<SampleComponent /> {/* このコンポーネント内でuseMediaQueryを試す */}
</Box>
);
}
基本的な動作を確認
stc/app/SampleComponent.tsx
'use client'; // 必須
import { Box, Theme, Typography, useMediaQuery, useTheme } from '@mui/material';
import { grey } from '@mui/material/colors';
/**
* 画面幅が600px以上か否かの状態(matches)を取得する
*/
export const SampleComponent = () => {
// const matches = useMediaQuery('(min-width:600px)'); // これでもOK
// const theme = useTheme();
// const matches = useMediaQuery(theme.breakpoints.up('sm')); // これでもOK
const matches = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm')); // ThemeProvider配下であればこれでもOK
// matchesはtrueかfalse
return (
<Box width={400} sx={{ backgroundColor: grey[200] }}>
<Typography variant="h5">{`(min-width:600px) matches: ${matches}`}</Typography>
</Box>
);
};
- React Developer Tools で確認するとウィンドウサイズが600pxをクロスする際にのみコンポーネントが再レンダリング(ハイライト)されていることが分かる。
- 上記gifには映っていないがSSRの結果としてはmatchの値はデフォルトのfalseである(つまり600px以上の画面でリロードすると一瞬falseが表示されたのちにtrue表示となる)。このデフォルト値は以下のように
createTheme
内で上書きできる。
export const theme = createTheme({
components: {
MuiUseMediaQuery: {
defaultProps: {
defaultMatches: true, // サーバーサイドレンダリング時にtrueを返す
},
},
},
});
応用(カスタムフック、プロバイダー)
Next.js(React)&MUIのレスポンシブデザインの仕組み作りの1例として、画面幅が3種類のデバイス幅のうちのどれであるかを監視、提供するカスタムフック、プロバイダーをuseMediaQueryを使用して作ってみる。
src/components/providers/ResponsiveProvider.tsx
'use client'; // 必須
import { Theme, useMediaQuery } from '@mui/material';
import { ReactNode, createContext, useContext } from 'react';
// 3種それぞれの画面幅であるか否かを管理するReact Contextを作成
const ResponsiveContext = createContext<{ isMobile: boolean; isTablet: boolean; isDesktop: boolean } | undefined>(
undefined
);
// React Contextを使ってプロバイダーコンポーネントを作成
export const ResponsiveProvider = ({ children }: { children: ReactNode }) => {
// 常に以下のうちの1つだけがtrueになるようにブレークポイントを設定
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'));
const isTablet = useMediaQuery((theme: Theme) => theme.breakpoints.between('sm', 'md'));
const isDesktop = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'));
return (
<ResponsiveContext.Provider value={{ isMobile, isTablet, isDesktop }}>{children}</ResponsiveContext.Provider>
);
};
// React Contextの値をデバイスタイプに変換して返すフックを作成
export const useResponsive = () => {
const context = useContext(ResponsiveContext);
if (context === undefined) {
// ResponsiveProviderコンポーネント配下からの呼び出しでない場合とみなす
throw new Error('useResponsive must be used within a ResponsiveProvider');
}
const { isMobile, isTablet, isDesktop } = context;
let deviceType: DeviceType = 'unknown';
if (isMobile) {
deviceType = 'mobile';
} else if (isTablet) {
deviceType = 'tablet';
} else if (isDesktop) {
deviceType = 'desktop';
}
return deviceType;
};
export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
src/app/layout.tsx(修正)
import { ResponsiveProvider } from '@/components/providers/ResponsiveProvider';
import { CssBaseline, ThemeProvider } from '@mui/material';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
import { theme } from './styles/theme';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</head>
<body>
<CssBaseline />
<ThemeProvider theme={theme}>
<ResponsiveProvider> {/* ←追加 */}
<AppRouterCacheProvider>{children}</AppRouterCacheProvider>
</ResponsiveProvider>
</ThemeProvider>
</body>
</html>
);
}
src/app/SampleComponent.tsx(修正)
'use client'; // 必須
import { useResponsive } from '@/components/providers/ResponsiveProvider';
import { Box, Typography } from '@mui/material';
import { grey } from '@mui/material/colors';
/**
* 画面幅がモバイル幅、タブレット幅、デスクトップ幅のいずれであるかをを取得する
*/
export const SampleComponent = () => {
const deviceType = useResponsive();
return (
<Box width={600} sx={{ backgroundColor: grey[200] }}>
<Typography variant="h6">{`(max-width:599px) isMobile: ${deviceType === 'mobile'}`}</Typography>
<Typography variant="h6">{`(min-width:600px) and (max-width:899px) isTablet: ${
deviceType === 'tablet'
}`}</Typography>
<Typography variant="h6">{`(min-width:900px) isTablet: ${deviceType === 'desktop'}`}</Typography>
{deviceType === 'mobile' && <Typography variant="h6">モバイルサイズ</Typography>}
{deviceType === 'tablet' && <Typography variant="h6">タブレットサイズ</Typography>}
{deviceType === 'desktop' && <Typography variant="h6">デスクトップサイズ</Typography>}
</Box>
);
};
- この例ではSSR時点では3種のデバイス全てがfalseである(deviceTypeが'unkown')。そのためSSR時点では「モバイルサイズ」「タブレットサイズ」「デスクトップサイズ」のいずれのDOM要素も生成されていない。SEOが重要な場合は
deviceType === 'unkown'
の際にもいずれかのデバイス幅向けのコンテンツがSSR時点で生成されるようにしておいた方が良いだろう。
Discussion