英文:
How to implement Material UI's `useMediaQuery` in NextJs(13.4) `/app` route to avoid flash?
问题
我目前正在进行一个Next.js项目,并且使用'app'路由器来在所有页面上设置通用布局。为了处理响应式设计,我正在使用Material-UI(MUI)及其useMediaQuery hook。然而,我遇到了一个问题,在页面加载的初始一秒钟内,在桌面设备上会出现移动版本的短暂闪烁。
这是我的设置概述:
- 使用Next.js
13.4.4
版本和MUI的/app
路由。 - 使用NextJS的
"use client"
指令引入客户端组件。
以下是代码部分:
"use client";
import { useState } from "react";
import { NextLinkComposed } from "../Link";
import { useTheme } from "@mui/material/styles";
import { drawerWidth } from "@/utils/constraints";
import {
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Drawer,
Box,
useMediaQuery,
InputAdornment,
OutlinedInput,
} from "@mui/material";
import {
GroupsOutlined as GroupsIcon,
DashboardOutlined as DashboardIcon,
Search as SearchIcon,
} from "@mui/icons-material";
export default function SideBar({
handleDrawerToggle,
drawerOpen,
}: {
handleDrawerToggle: () => void;
drawerOpen: boolean;
}) {
const theme = useTheme();
const matchUpMd = useMediaQuery(theme.breakpoints.up("md"));
const container =
typeof window !== undefined ? () => window.document.body : undefined;
const [searchValue, setSearchValue] = useState("");
const filteredItems = SideBarItems.filter((item) =>
item.name.toLowerCase().includes(searchValue.toLowerCase())
);
const drawer = (
<Box
component="div"
sx={{
marginTop: { md: "66px", xs: "12px" },
paddingLeft: "16px",
paddingRight: "16px",
}}
>
<Box>
<OutlinedInput
size="small"
id="input-search-header"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{
borderRadius: 3,
}}
placeholder="Search"
startAdornment={
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
}
aria-describedby="search-helper-text"
inputProps={{ "aria-label": "weight" }}
/>
</Box>
<List sx={{ marginTop: 2 }}>
{filteredItems.map((item, index) => {
const Icon = item.icon;
return (
<ListItem disablePadding key={index}>
<ListItemButton
sx={{ borderRadius: 3 }}
component={NextLinkComposed}
to={{
pathname: item.path,
}}
>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={item.name} />
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
);
return (
<Box
component="nav"
sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : "auto" }}
aria-label="mailbox folders"
>
<Drawer
container={container}
variant={matchUpMd ? "persistent" : "temporary"}
open={drawerOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // 移动端的性能更好。
}}
sx={{
"& .MuiDrawer-paper": {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRight: "none",
},
}}
>
{drawer}
</Drawer>
</Box>
);
}
const SideBarItems = [
{
name: "Dashboard",
path: "/dashboard",
icon: DashboardIcon,
},
{ name: "Users", path: "/user", icon: GroupsIcon },
];
请帮忙!谢谢。
英文:
I'm currently working on a Next.js project and utilizing the 'app' router to set up a common layout across all pages. To handle responsive design, I'm using Material-UI (MUI) and its useMediaQuery hook. However, I'm encountering an issue where there's a brief flash screen of the mobile version on the desktop device during the initial second of the render when load a page.
Here's an overview of my setup:
- MUI with NextJS
13.4.4
/app
route. - Using Client Component By
"use client";
directives of NextJS.
Here is the Code:
"use client";
import { useState } from "react";
import { NextLinkComposed } from "../Link";
import { useTheme } from "@mui/material/styles";
import { drawerWidth } from "@/utils/constraints";
import {
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Drawer,
Box,
useMediaQuery,
InputAdornment,
OutlinedInput,
} from "@mui/material";
import {
GroupsOutlined as GroupsIcon,
DashboardOutlined as DashboardIcon,
Search as SearchIcon,
} from "@mui/icons-material";
export default function SideBar({
handleDrawerToggle,
drawerOpen,
}: {
handleDrawerToggle: () => void;
drawerOpen: boolean;
}) {
const theme = useTheme();
const matchUpMd = useMediaQuery(theme.breakpoints.up("md"));
const container =
typeof window !== undefined ? () => window.document.body : undefined;
const [searchValue, setSearchValue] = useState("");
const filteredItems = SideBarItems.filter((item) =>
item.name.toLowerCase().includes(searchValue.toLowerCase())
);
const drawer = (
<Box
component="div"
sx={{
marginTop: { md: "66px", xs: "12px" },
paddingLeft: "16px",
paddingRight: "16px",
}}
>
<Box>
<OutlinedInput
size="small"
id="input-search-header"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{
borderRadius: 3,
}}
placeholder="Search"
startAdornment={
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
}
aria-describedby="search-helper-text"
inputProps={{ "aria-label": "weight" }}
/>
</Box>
<List sx={{ marginTop: 2 }}>
{filteredItems.map((item, index) => {
const Icon = item.icon;
return (
<ListItem disablePadding key={index}>
<ListItemButton
sx={{ borderRadius: 3 }}
component={NextLinkComposed}
to={{
pathname: item.path,
}}
>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={item.name} />
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
);
return (
<Box
component="nav"
sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : "auto" }}
aria-label="mailbox folders"
>
<Drawer
container={container}
variant={matchUpMd ? "persistent" : "temporary"}
open={drawerOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
"& .MuiDrawer-paper": {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRight: "none",
},
}}
>
{drawer}
</Drawer>
</Box>
);
}
const SideBarItems = [
{
name: "Dashboard",
path: "/dashboard",
icon: DashboardIcon,
},
{ name: "Users", path: "/user", icon: GroupsIcon },
];
PLEASE HELP!
Thank You.
答案1
得分: 1
以下是您要翻译的内容:
我有一个针对这种情况的解决方法(我使用应用程序路由器和SSG渲染)。 基于MUI官方文档中的这篇文章(https://mui.com/material-ui/guides/next-js-app-router/#next-js-and-react-server-components)。
这种方法需要在第一次加载时将一个状态设置为localStorage中的deviceWidth。 因为localStorage是同步的,所以它不会有任何阻塞。
ThemeProvider.tsx
"use client";
import createCache from "@emotion/cache";
import { useServerInsertedHTML } from "next/navigation";
import { CacheProvider } from "@emotion/react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import React from "react";
import mediaQuery from "css-mediaquery";
const theme = createTheme({
components: {
MuiUseMediaQuery: {
defaultProps: {
ssrMatchMedia: (query) => ({
matches: mediaQuery.match(query, {
// 浏览器的估计CSS宽度。
width: localStorage.getItem("deviceWidth") || 1200, // 添加此默认属性
}),
}),
},
},
},
});
export default function ThemeRegistry(props: any) {
const { options, children } = props;
const [{ cache, flush }] = React.useState(() => {
const cache = createCache(options);
cache.compat = true;
const prevInsert = cache.insert;
let inserted: string[] = [];
cache.insert = (...args) => {
const serialized = args[1];
if (cache.inserted[serialized.name] === undefined) {
inserted.push(serialized.name);
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const names = flush();
if (names.length === 0) {
return null;
}
let styles = "";
for (const name of names) {
styles += cache.inserted[name];
}
return (
<style
key={cache.key}
data-emotion={`${cache.key} ${names.join(" ")}`}
dangerouslySetInnerHTML={{
__html: styles,
}}
/>
);
});
return (
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</CacheProvider>
);
}
layout.tsx
import ThemeRegistry from "./ThemeRegistry";
import "./styles.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
</head>
<body>
<ThemeRegistry options={{ key: "mui" }}>{children}</ThemeRegistry>
</body>
</html>
);
}
英文:
I have a workaround approach for this case (I use app router & SSG rendering). Based on this article from MUI official docs (https://mui.com/material-ui/guides/next-js-app-router/#next-js-and-react-server-components).
This approach need to set a state in localStorage as deviceWidth in the first load. Because localStorage is synchronous, it doesn't have any blocking.
ThemeProvider.tsx
"use client";
import createCache from "@emotion/cache";
import { useServerInsertedHTML } from "next/navigation";
import { CacheProvider } from "@emotion/react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import React from "react";
import mediaQuery from "css-mediaquery";
const theme = createTheme({
components: {
MuiUseMediaQuery: {
defaultProps: {
ssrMatchMedia: (query) => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: localStorage.getItem("deviceWidth") || 1200, // Add this default prop
}),
}),
},
},
},
});
export default function ThemeRegistry(props: any) {
const { options, children } = props;
const [{ cache, flush }] = React.useState(() => {
const cache = createCache(options);
cache.compat = true;
const prevInsert = cache.insert;
let inserted: string[] = [];
cache.insert = (...args) => {
const serialized = args[1];
if (cache.inserted[serialized.name] === undefined) {
inserted.push(serialized.name);
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const names = flush();
if (names.length === 0) {
return null;
}
let styles = "";
for (const name of names) {
styles += cache.inserted[name];
}
return (
<style
key={cache.key}
data-emotion={`${cache.key} ${names.join(" ")}`}
dangerouslySetInnerHTML={{
__html: styles,
}}
/>
);
});
return (
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</CacheProvider>
);
}
layout.tsx
import ThemeRegistry from "./ThemeRegistry";
import "./styles.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
</head>
<body>
<ThemeRegistry options={{ key: "mui" }}>{children}</ThemeRegistry>
</body>
</html>
);
}
答案2
得分: -1
你可以使用 MUI 的隐藏组件方法查看这里。您可以创建两个单独的抽屉,并根据断点使用 sx 属性来隐藏它们。
英文:
You can you use mui hidden component approach see here.You can create two seperate drawers and hide them acc to breakpoints using sx props.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论