-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Next.js 13 example with the app
router
#34898
Comments
Also Check https://nextjs.org/blog/next-13#nextlink Code example affected material-ui/examples/nextjs-with-typescript/src/Link.tsx Lines 8 to 37 in 012c95f
Temporary workaroundadd prop |
Another workaround for |
Probably for the full compatibility of Mui 5 with NextJs 13, Emotion must have support for React Server Components and Concurrent Rendering. This already has been requested at Emotion's repo: emotion-js/emotion#2928 |
Is there any workaround that would enable us to use the |
This comment was marked as resolved.
This comment was marked as resolved.
@mnajdova I don't think this is a duplicate of #34893 I think those are completely different requests/issues. |
I thought this commit would be a good read at least for the
|
I think the NextJS examples needs to be split into two separate examples: |
Wouldnt it make sense to pin the versions of the used libs at least to the major versions? |
Locally in your projects yes. In our examples, we intentionally use |
For I think one option to properly migrate to v13 would be to remove the need for And as for |
@PetroSilenius I just got this link from my colleague: https://mui.com/material-ui/guides/routing/#global-theme-link |
Yea the global theme Link is a great tool! It might be a bit opionated for the nextjs examples though as there are some cases where NextLink doesn't behave as needed for example with external links with a certain target. Could point it up better in the docs though👍🏻 |
@PetroSilenius I'm going to add a check if the href is relative, if so use NextLink otherwise use an a. moving that logic to the theme saves a lot of boilerplate. |
Can workaround the "use client" // <--- this
import {Button} from "@mui/material" And then use this new file to import the component. I don't like it but if maybe someone has no choice and has to move it NOW to next 13, then he could re-export it this way... There are sure way make it less of a pain to undo later. |
I work on the following boilerplate, and had to self-solve most of these issues. https://github.com/Rethunk-Tech/nextjs-boilerplate Is there any more progress on this issue I should be aware of? Is it better to track #34905 instead? |
up
Hey, did you find any solutions? |
@cristianbuta @danielcolgan hey guys 😄 yes, but I was lazy to report back here, though I expected such questions to appear 😄 let me try to summarize my setup (using next 13.2.1, so with new metadata structure = no // src/app/layout.tsx
import { Metadata } from "next/dist/lib/metadata/types/metadata-interface"
import { MuiSetup } from "./MuiSetup"
export const metadata: Metadata = {
title: "My title",
description: "My description",
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<MuiSetup>{children}</MuiSetup>
</body>
</html>
)
} // src/app/theme.ts
import { createTheme } from "@mui/material"
export const theme = createTheme() // src/app/MuiSetup.tsx
"use client"
import { CssBaseline, ThemeProvider } from "@mui/material"
import { ReactNode } from "react"
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir"
import { theme } from "./theme"
type Props = {
children: ReactNode
}
export const MuiSetup = ({ children }: Props) => {
return (
<>
<CssBaseline />
{/* MUI (but actually underlying Emotion) isn't ready to work with Next's experimental `app/` directory feature.
I'm using the lowest-code approach suggested by this guy here: https://github.com/emotion-js/emotion/issues/2928#issuecomment-1386197925 */}
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</NextAppDirEmotionCacheProvider>
</>
)
} // src/app/page.tsx
import { Home } from "./Home"
export default function Page() {
return <Home />
} // src/app/Home.tsx
"use client"
import { Stack, Typography } from "@mui/material"
export const Home = () => (
<Stack sx={{ minHeight: "100vh", padding: "64px" }}>
<Stack sx={{ flexGrow: 1, justifyContent: "center", alignItems: "center" }} component="main">
<Typography variant="h5">main content</Typography>
</Stack>
<Stack direction="row" sx={{ justifyContent: "space-between" }} component="footer">
<Typography>footer</Typography>
<Typography>stuff</Typography>
</Stack>
</Stack>
) these 5 files are all the files I have in the |
I am using next-themes and Tailwind to add a dark mode option. Based on the code provided by @rtrembecky I made this example: // tailwind.config.js
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: "class",
content: ["./src/app/**/*.tsx"],
theme: {
extend: {},
},
variants: {},
plugins: [],
};
module.exports = config; /* src/app/globals.css */
@tailwind components;
@tailwind utilities; // src/app/layout.tsx
import "./globals.css";
import Providers from "./Providers";
export const metadata = {
title: "My title",
description: "My description",
};
const RootLayout = ({ children }: { children: React.ReactNode }) => (
// suppressHydrationWarning is for next-themes - see: https://github.com/pacocoursey/next-themes#with-app
<html lang="en" suppressHydrationWarning>
<head />
<body>
<Providers>{children}</Providers>
</body>
</html>
);
export default RootLayout; // src/app/theme.ts
export const DEFAULT_THEME: "dark" | "light" = "dark";
export const getOtherTheme = (theme: string | undefined): "dark" | "light" => {
switch (theme) {
case "dark":
return "light";
case "light":
return "dark";
case "system":
default:
return DEFAULT_THEME;
}
}; // src/app/Providers.tsx
"use client";
import { createTheme as createMuiTheme, ThemeProvider as MuiThemeProvider } from "@mui/material";
import { ThemeProvider, useTheme } from "next-themes";
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
import { DEFAULT_THEME } from "./theme";
const MuiProvider = ({ children }: { children: React.ReactNode }) => {
const { theme: themeState } = useTheme();
const themeName = themeState === "dark" || themeState === "light" ? themeState : DEFAULT_THEME;
const theme = createMuiTheme({ palette: { mode: themeName } });
return (
// CssBaseline causes the theme switch to stop working
<NextAppDirEmotionCacheProvider options={{ key: "css" }}>
<MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
</NextAppDirEmotionCacheProvider>
);
};
const NextThemeProvider = ({ children }: { children: React.ReactNode }) => (
// Separate next-themes Provider from MUI, so is does not get rerendered on theme switch
<ThemeProvider attribute="class" defaultTheme={DEFAULT_THEME}>
{children}
</ThemeProvider>
);
const Providers = ({ children }: { children: React.ReactNode }) => (
<NextThemeProvider>
<MuiProvider>{children}</MuiProvider>
</NextThemeProvider>
);
export default Providers; // src/app/page.tsx
import MuiContent from "./MuiContent";
import ThemeButton from "./ThemeButton";
const Index = () => (
<>
<ThemeButton />
<MuiContent />
</>
);
export default Index; // src/app/ThemeButton.tsx
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { DEFAULT_THEME, getOtherTheme } from "./theme";
const ThemeButton = () => {
const { theme: themeState, setTheme } = useTheme();
const [themeName, setThemeName] = useState(DEFAULT_THEME);
useEffect(() => setThemeName(getOtherTheme(themeState)), [themeState]);
return (
<button type="button" onClick={() => setTheme(getOtherTheme(themeState))}>
{`Activate ${themeName} Theme`}
</button>
);
};
export default ThemeButton; // src/app/MuiContent.tsx
"use client";
import { Stack, Typography } from "@mui/material";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { DEFAULT_THEME, validThemeOrDefault } from "@/theme";
const MuiContent = () => {
const { theme } = useTheme();
const [themeName, setThemeName] = useState(DEFAULT_THEME);
useEffect(() => setThemeName(validThemeOrDefault(theme)), [theme]);
return (
<Stack sx={{ minHeight: "100vh", padding: "64px" }}>
<Stack sx={{ flexGrow: 1, justifyContent: "center", alignItems: "center" }} component="main">
<Typography variant="h5">main content with Theme: {`${themeName}`}</Typography>
</Stack>
<Stack direction="row" sx={{ justifyContent: "space-between" }} component="footer">
<Typography>footer</Typography>
<Typography>stuff</Typography>
</Stack>
</Stack>
);
};
export default MuiContent; |
I tested @rtrembecky's sample code, and it worked, but then I tried a different way of using MUI components in application pages. The This file exports all Material content but has a "use client" line, which forces all components to be client side. // src/components/@mui/material/index.tsx
'use client';
export * from '@mui/material'; On the page we can import the components as in Material. It can still be a server component, and we also don't need to group the material components into a different component with the "use client" line. // src/app/page.tsx
import { Alert, AlertTitle, Button, Container } from '../components/@mui/material';
export default function Home() {
return (
<Container>
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
This is an error alert with title — check it out!
</Alert>
<Alert severity="success">
<AlertTitle>Success</AlertTitle>
This is a success alert with title — check it out!
</Alert>
<Button variant="contained">
Contained
</Button>
</Container>
);
} We can also create specific files to export each material component, which reduces page load time by about 0.4s in my tests. Each file looks like this: // src/components/@mui/material/Alert.tsx
'use client';
import Alert from '@mui/material/Alert';
export default Alert; And we can import the component like in material with: import Alert from '@/components/@mui/material/Alert'; The code seems to work fine. I don't know if this decreases performance as I am creating server components which in some cases may have many client components inside it. If anyone has an opinion on this, I'd like to know. |
@renanrms this worked great! thanks for the suggestion. I used the simple thanks again 🎉 and thanks @rtrembecky for the original suggestion / workaround! |
@Cielquan Thanks for the example. Did you encounter this error? For some reason when generating the theme based on the selected theme name (light/dark) it fails to match the classnames between re-renders. Also, the CssBaseline is required, otherwise styles such as typography are broken. Did you manage to find a solution for that? |
@arobert93, TL;DR example repo I created link that fixes issues with edit the repo has been updated to include
Looks like someone else had an idea around cookie usage over on your other post link
I experienced the same. I found this link to a codesandbox that used some experimental features but the CodeSB didnt work out of the box (outdated dependency issues). I forked it and created a repo you can check out link. It is basic and just uses the system color scheme (no theme changing button etc) but i didnt need to include I did not update the example to use @renanrms idea but the example repo should be enough for a jumping off point, will try to update it with all the bells and whistles at a later point hope this helps 🎉 |
I just did a little testing and tried to get all my prior used providers to work, when I came up with my solution above. I did not notice any issues with the Typography component yet. Unfortunately I will have no time to dig into the topic again more deeply for some weeks. |
Hi Since NextJS 13.4 is now released, which claims the app router directory to be stable, when can we expect an official example code to use MUI with NextJS for server components? |
Good day everyone! I am also curious to see some examples on how to transition from pages to app directory. I am working on a project using a template from https://minimals.cc. Currently, in the pages folder, my _app.tsx, _document.tsx, and index.tsx appears as follows: // _app.tsx
// i18n
import '../locales/i18n';
// scroll bar
import 'simplebar-react/dist/simplebar.min.css';
// lightbox
import 'yet-another-react-lightbox/styles.css';
import 'yet-another-react-lightbox/plugins/captions.css';
import 'yet-another-react-lightbox/plugins/thumbnails.css';
// map
import 'mapbox-gl/dist/mapbox-gl.css';
// editor
import 'react-quill/dist/quill.snow.css';
// slick-carousel
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
// lazy image
import 'react-lazy-load-image-component/src/effects/blur.css';
// ----------------------------------------------------------------------
import { CacheProvider, EmotionCache } from '@emotion/react';
// next
import { NextPage } from 'next';
import Head from 'next/head';
import { AppProps } from 'next/app';
// redux
import { Provider as ReduxProvider } from 'react-redux';
// @mui
import { LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
// redux
import { store } from '../redux/store';
// utils
import createEmotionCache from '../utils/createEmotionCache';
// theme
import ThemeProvider from '../theme';
// locales
import ThemeLocalization from '../locales';
// components
import { StyledChart } from '../components/chart';
import ProgressBar from '../components/progress-bar';
import SnackbarProvider from '../components/snackbar';
import { MotionLazyContainer } from '../components/animate';
import { ThemeSettings, SettingsProvider } from '../components/settings';
// Check our docs
// https://docs.minimals.cc/authentication/ts-version
import { AuthProvider } from '../auth/JwtContext';
// import { AuthProvider } from '../auth/Auth0Context';
// import { AuthProvider } from '../auth/FirebaseContext';
// import { AuthProvider } from '../auth/AwsCognitoContext';
// ----------------------------------------------------------------------
const clientSideEmotionCache = createEmotionCache();
type NextPageWithLayout = NextPage & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
};
interface MyAppProps extends AppProps {
emotionCache?: EmotionCache;
Component: NextPageWithLayout;
}
export default function MyApp(props: MyAppProps) {
const { Component, pageProps, emotionCache = clientSideEmotionCache } = props;
const getLayout = Component.getLayout ?? ((page) => page);
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<AuthProvider>
<ReduxProvider store={store}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<SettingsProvider>
<MotionLazyContainer>
<ThemeProvider>
<ThemeSettings>
<ThemeLocalization>
<SnackbarProvider>
<StyledChart />
<ProgressBar />
{getLayout(<Component {...pageProps} />)}
</SnackbarProvider>
</ThemeLocalization>
</ThemeSettings>
</ThemeProvider>
</MotionLazyContainer>
</SettingsProvider>
</LocalizationProvider>
</ReduxProvider>
</AuthProvider>
</CacheProvider>
);
} // _document.tsx
import * as React from 'react';
// next
import Document, { Html, Head, Main, NextScript } from 'next/document';
// @emotion
import createEmotionServer from '@emotion/server/create-instance';
// utils
import createEmotionCache from '../utils/createEmotionCache';
// theme
import palette from '../theme/palette';
import { primaryFont } from '../theme/typography';
// ----------------------------------------------------------------------
export default class MyDocument extends Document {
render() {
return (
<Html lang="en" className={primaryFont.className}>
<Head>
<meta charSet="utf-8" />
<link rel="manifest" href="/manifest.json" />
{/* PWA primary color */}
<meta name="theme-color" content={palette('light').primary.main} />
{/* Favicon */}
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
{/* Emotion */}
<meta name="emotion-insertion-point" content="" />
{(this.props as any).emotionStyleTags}
{/* Meta */}
<meta
name="description"
content="Alert 360 Home Security Systems up to 25% off RETAIL! Save big on Alarm Monitoring starting at $15.95 & Top-Rated Security Cameras!"
/>
<meta name="keywords" content="home,security,alarm,system,wireless,monitoring,cameras" />
<meta name="author" content="Alert 360" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// ----------------------------------------------------------------------
MyDocument.getInitialProps = async (ctx) => {
const originalRenderPage = ctx.renderPage;
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) =>
function EnhanceApp(props) {
return <App emotionCache={cache} {...props} />;
},
});
const initialProps = await Document.getInitialProps(ctx);
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map((style) => (
<style
data-emotion={`${style.key} ${style.ids.join(' ')}`}
key={style.key}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style.css }}
/>
));
return {
...initialProps,
emotionStyleTags,
};
}; // index.tsx
// next
import Head from 'next/head';
// @mui
import { Box } from '@mui/material';
// layouts
import MainLayout from '../layouts/main';
// components
import ScrollProgress from '../components/scroll-progress';
// sections
import {
HomeHero,
HomeMinimal,
HomeDarkMode,
HomeLookingFor,
HomeForDesigner,
HomeColorPresets,
HomePricingPlans,
HomeAdvertisement,
HomeCleanInterfaces,
HomeHugePackElements,
} from '../sections/home';
// ----------------------------------------------------------------------
HomePage.getLayout = (page: React.ReactElement) => <MainLayout> {page} </MainLayout>;
// ----------------------------------------------------------------------
export default function HomePage() {
return (
<>
<Head>
<title> The starting point for your next project | Minimal UI</title>
</Head>
<ScrollProgress />
<HomeHero />
<Box
sx={{
overflow: 'hidden',
position: 'relative',
bgcolor: 'background.default',
}}
>
<HomeMinimal />
<HomeHugePackElements />
<HomeForDesigner />
<HomeDarkMode />
<HomeColorPresets />
<HomeCleanInterfaces />
<HomePricingPlans />
<HomeLookingFor />
<HomeAdvertisement />
</Box>
</>
);
} What would the new layout.tsx, MuiSetup.tsx, theme.ts, and page.tsx (in the app folder) look like? Looking at the outstanding contributions by others in this thread, I have an idea and will give it a try today!! Thank you. :) |
Here is an example to look at using the Next.js's |
So basically, does the whole application become a client side application then with 'use client'; in every single component??! |
Setup the environment for testing and to leverage a component library for expedience of building the site. It should, also, be noted that MUI can only be used when a component is rendered client-side, instead of, the prefered server-side. This is issue accures as of Next.js 13, and is explained in further detail: mui/material-ui#34898, mui/material-ui#37315
@alaindeurveilher Really weird. Seems absurd that to use the new RSC version of NextJS, we have to make our whole app client. Either the MUI doc is not understood or it is the NextJS one ; MUI seems compatible with a server side render: https://mui.com/material-ui/guides/server-rendering/ I have to prepare some POC for the teams of my company, I'll give it a try this weekend. |
Guess we're not using Material-UI for new projects on NextJS 13 in the meantime. |
@alaindeurveilher I have misunderstood; using @AlbinoGeek I thought the same thing at first. But in the "Good to know" part from the doc above, they say:
So we could be good, I'll try it. 👀 |
I wanted to use MUI with Tailwind CSS. This is what I was able to do, though I am not sure if everything is implemented fine or not: layout.tsx import "./globals.css";
import Navigation from "./Navigation";
import ThemeWrapper from "./ThemeWrapper";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
let menuLinks: { title: string; url: string }[] = [
{ title: "Home", url: "#" },
{ title: "Contact", url: "#" },
];
return (
<html lang="en">
<ThemeWrapper>
<body>
<Navigation menuLinks={menuLinks} />
{children}
</body>
</ThemeWrapper>
</html>
);
} globals.css
ThemeWrapper.tsx "use client";
import {
CssBaseline,
StyledEngineProvider,
ThemeProvider,
createTheme,
} from "@mui/material";
export default function ThemeWrapper({ children }: { children: any }) {
const theme = createTheme({});
return (
<StyledEngineProvider injectFirst>
<CssBaseline />
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</StyledEngineProvider>
);
} And then I'm seemingly able to use Tailwind css and mui components pretty easily. Here is an example component: Hero.tsx "use client"
...
export default function MainHero() {
const theme = useTheme();
return (
<div className="relative isolate overflow-hidden bg-gray-900">
<svg
className="absolute inset-0 -z-10 h-full w-full stroke-white/10 [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)]"
aria-hidden="true"
>
// more stuff
</div>
);
} tailwind.config.js /** @type {import('tailwindcss').Config} */
module.exports = {
corePlugins: {
preflight: false,
},
...
} |
@Tigatok It seems to me that this approach forces all children components in the body to be client-sided, or am I overlooking something? |
app
router
This PR with app router example looks close to merge #37315 |
I feel like, forcing every MUI component to render client side with |
@Seanmclem I agree with what I interpret as the core of your argument (that MUI should make styling possible in server-only components aka React Server Components aka RSC), but it sounds like you're mixing things up - SSR is not the same as what Server Components provide. I think you may mean "Server Components-first", not "SSR-first" (since |
Hey guys, sorry I was not on track, is this issue still there with app router? |
This might be helpful if you are using with tailwind css - https://ui.shadcn.com |
Duplicates
Latest version
Summary 💡
There is a great example of how to use MUI with next.js (https://github.com/mui/material-ui/blob/master/examples/nextjs-with-typescript) in this repository.
Yet, a new version of Next.js was released recently: https://nextjs.org/blog/next-13
This introduces several new concepts and ways to write React apps with server-side components in mind. To migrate from the old Next.js to the new version, one needs to do several things:
app/
directory instead ofpages/
(although this can be done incrementally)app/
directory, one has to usefetch
(or any otherasync
/await
-based data fetching method instead ofgetInitialProps
,getServerSideProps
, orgetStaticProps
._document.tsx
and_app.tsx
should be refactored into a globallayout.tsx
: https://beta.nextjs.org/docs/upgrade-guide#migrating-_documentjs-and-_appjsIt would be great to get directions on how to use MUI with emotion styles and cache in this scenario.
Would be even better to have a sample app like the one above.
Examples 🌈
No response
Motivation 🔦
Next.js 13 is a major new version that introduces several exciting features that would be really great to use in our apps.
It would be great to have a guide or directions on how to integrate MUI with the new version of Next.js instead of the old
_document
and_app
based one.I think there will be many developers that would want to update to the newer version and might get stuck (like myself) on the MUI integration and how the emotion styles and cache work on this new "server-first" environment without
getInitialProps
.The text was updated successfully, but these errors were encountered: