如何在Next.js(13.4)的`/app`路由中实现Material UI的`useMediaQuery`,以避免闪烁?

huangapple go评论60阅读模式
英文:

How to implement Material UI's `useMediaQuery` in NextJs(13.4) `/app` route to avoid flash?

问题

我目前正在进行一个Next.js项目,并且使用'app'路由器来在所有页面上设置通用布局。为了处理响应式设计,我正在使用Material-UI(MUI)及其useMediaQuery hook。然而,我遇到了一个问题,在页面加载的初始一秒钟内,在桌面设备上会出现移动版本的短暂闪烁。

这是我的设置概述:

  1. 使用Next.js 13.4.4 版本和MUI的/app路由。
  2. 使用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:

  1. MUI with NextJS 13.4.4 /app route.
  2. Using Client Component By &quot;use client&quot;; directives of NextJS.

Here is the Code:

 &quot;use client&quot;;
import { useState } from &quot;react&quot;;
import { NextLinkComposed } from &quot;../Link&quot;;
import { useTheme } from &quot;@mui/material/styles&quot;;
import { drawerWidth } from &quot;@/utils/constraints&quot;;
import {
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Drawer,
Box,
useMediaQuery,
InputAdornment,
OutlinedInput,
} from &quot;@mui/material&quot;;
import {
GroupsOutlined as GroupsIcon,
DashboardOutlined as DashboardIcon,
Search as SearchIcon,
} from &quot;@mui/icons-material&quot;;
export default function SideBar({
handleDrawerToggle,
drawerOpen,
}: {
handleDrawerToggle: () =&gt; void;
drawerOpen: boolean;
}) {
const theme = useTheme();
const matchUpMd = useMediaQuery(theme.breakpoints.up(&quot;md&quot;));
const container =
typeof window !== undefined ? () =&gt; window.document.body : undefined;
const [searchValue, setSearchValue] = useState(&quot;&quot;);
const filteredItems = SideBarItems.filter((item) =&gt;
item.name.toLowerCase().includes(searchValue.toLowerCase())
);
const drawer = (
&lt;Box
component=&quot;div&quot;
sx={{
marginTop: { md: &quot;66px&quot;, xs: &quot;12px&quot; },
paddingLeft: &quot;16px&quot;,
paddingRight: &quot;16px&quot;,
}}
&gt;
&lt;Box&gt;
&lt;OutlinedInput
size=&quot;small&quot;
id=&quot;input-search-header&quot;
value={searchValue}
onChange={(e) =&gt; setSearchValue(e.target.value)}
sx={{
borderRadius: 3,
}}
placeholder=&quot;Search&quot;
startAdornment={
&lt;InputAdornment position=&quot;start&quot;&gt;
&lt;SearchIcon /&gt;
&lt;/InputAdornment&gt;
}
aria-describedby=&quot;search-helper-text&quot;
inputProps={{ &quot;aria-label&quot;: &quot;weight&quot; }}
/&gt;
&lt;/Box&gt;
&lt;List sx={{ marginTop: 2 }}&gt;
{filteredItems.map((item, index) =&gt; {
const Icon = item.icon;
return (
&lt;ListItem disablePadding key={index}&gt;
&lt;ListItemButton
sx={{ borderRadius: 3 }}
component={NextLinkComposed}
to={{
pathname: item.path,
}}
&gt;
&lt;ListItemIcon&gt;
&lt;Icon /&gt;
&lt;/ListItemIcon&gt;
&lt;ListItemText primary={item.name} /&gt;
&lt;/ListItemButton&gt;
&lt;/ListItem&gt;
);
})}
&lt;/List&gt;
&lt;/Box&gt;
);
return (
&lt;Box
component=&quot;nav&quot;
sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : &quot;auto&quot; }}
aria-label=&quot;mailbox folders&quot;
&gt;
&lt;Drawer
container={container}
variant={matchUpMd ? &quot;persistent&quot; : &quot;temporary&quot;}
open={drawerOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
&quot;&amp; .MuiDrawer-paper&quot;: {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRight: &quot;none&quot;,
},
}}
&gt;
{drawer}
&lt;/Drawer&gt;
&lt;/Box&gt;
);
}
const SideBarItems = [
{
name: &quot;Dashboard&quot;,
path: &quot;/dashboard&quot;,
icon: DashboardIcon,
},
{ name: &quot;Users&quot;, path: &quot;/user&quot;, 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

&quot;use client&quot;;
import createCache from &quot;@emotion/cache&quot;;
import { useServerInsertedHTML } from &quot;next/navigation&quot;;
import { CacheProvider } from &quot;@emotion/react&quot;;
import { ThemeProvider, createTheme } from &quot;@mui/material/styles&quot;;
import CssBaseline from &quot;@mui/material/CssBaseline&quot;;
import React from &quot;react&quot;;
import mediaQuery from &quot;css-mediaquery&quot;;
const theme = createTheme({
components: {
MuiUseMediaQuery: {
defaultProps: {
ssrMatchMedia: (query) =&gt; ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: localStorage.getItem(&quot;deviceWidth&quot;) || 1200, // Add this default prop
}),
}),
},
},
},
});
export default function ThemeRegistry(props: any) {
const { options, children } = props;
const [{ cache, flush }] = React.useState(() =&gt; {
const cache = createCache(options);
cache.compat = true;
const prevInsert = cache.insert;
let inserted: string[] = [];
cache.insert = (...args) =&gt; {
const serialized = args[1];
if (cache.inserted[serialized.name] === undefined) {
inserted.push(serialized.name);
}
return prevInsert(...args);
};
const flush = () =&gt; {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() =&gt; {
const names = flush();
if (names.length === 0) {
return null;
}
let styles = &quot;&quot;;
for (const name of names) {
styles += cache.inserted[name];
}
return (
&lt;style
key={cache.key}
data-emotion={`${cache.key} ${names.join(&quot; &quot;)}`}
dangerouslySetInnerHTML={{
__html: styles,
}}
/&gt;
);
});
return (
&lt;CacheProvider value={cache}&gt;
&lt;ThemeProvider theme={theme}&gt;
&lt;CssBaseline /&gt;
{children}
&lt;/ThemeProvider&gt;
&lt;/CacheProvider&gt;
);
}

layout.tsx

import ThemeRegistry from &quot;./ThemeRegistry&quot;;
import &quot;./styles.css&quot;;
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;ThemeRegistry options={{ key: &quot;mui&quot; }}&gt;{children}&lt;/ThemeRegistry&gt;
&lt;/body&gt;
&lt;/html&gt;
);
}

答案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.

huangapple
  • 本文由 发表于 2023年6月12日 22:44:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76457780.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定