-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
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
[examples] Next.js v13 app router with Material UI #37315
Changes from all commits
8c6e824
f6aaa91
9d37179
b4c8fdd
3479622
6d95877
69eb595
0cd3bbb
d4d0f72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
.pnpm-debug.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
# next-env.d.ts |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Material UI - Next.js App Router example in TypeScript | ||
|
||
## How to use | ||
|
||
Download the example [or clone the repo](https://github.com/mui/material-ui): | ||
|
||
<!-- #default-branch-switch --> | ||
|
||
```sh | ||
curl https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/material-next-ts | ||
cd material-next-app-router-ts | ||
``` | ||
|
||
Install it and run: | ||
|
||
```sh | ||
npm install | ||
npm run dev | ||
``` | ||
|
||
## The idea behind the example | ||
|
||
The project uses [Next.js](https://github.com/vercel/next.js), which is a framework for server-rendered React apps. | ||
It includes `@mui/material` and its peer dependencies, including [Emotion](https://emotion.sh/docs/introduction), the default style engine in Material UI v5. If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components). | ||
|
||
## What's next? | ||
|
||
<!-- #default-branch-switch --> | ||
|
||
You now have a working example project. | ||
You can head back to the documentation, continuing browsing it from the [templates](https://mui.com/material-ui/getting-started/templates/) section. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/basic-features/typescript for more information. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = { | ||
reactStrictMode: true, | ||
swcMinify: true, | ||
modularizeImports: { | ||
'@mui/icons-material': { | ||
transform: '@mui/icons-material/{{member}}', | ||
}, | ||
}, | ||
}; | ||
|
||
module.exports = nextConfig; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"name": "material-next-app-router-ts", | ||
"version": "5.0.0", | ||
"private": true, | ||
"scripts": { | ||
"dev": "next dev", | ||
"build": "next build", | ||
"start": "next start", | ||
"lint": "next lint", | ||
"post-update": "echo \"codesandbox preview only, need an update\" && yarn upgrade --latest" | ||
}, | ||
"dependencies": { | ||
"@emotion/cache": "latest", | ||
"@emotion/react": "latest", | ||
"@emotion/styled": "latest", | ||
"@mui/icons-material": "latest", | ||
"@mui/material": "latest", | ||
"next": "latest", | ||
"react": "latest", | ||
"react-dom": "latest" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "latest", | ||
"@types/react": "latest", | ||
"@types/react-dom": "latest", | ||
"eslint": "latest", | ||
"eslint-config-next": "latest", | ||
"typescript": "latest" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as React from 'react'; | ||
import About from '@/layouts/About/About'; | ||
|
||
export default function AboutPage() { | ||
return <About />; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Fonts Example | ||
import { Inter } from 'next/font/google'; | ||
|
||
const GoogleInterFont = Inter({ subsets: ['latin'] }); | ||
|
||
export default GoogleInterFont; | ||
|
||
// Local Fonts example | ||
// more details here: https://nextjs.org/docs/app/building-your-application/optimizing/fonts#local-fonts | ||
// import localFont from 'next/font/local'; | ||
// const LocalFont = localFont({src: [{path: './path-of-font-file-regular.woff', weight: '400', style: 'normal'}], fallback: ['Arial', 'sans-serif']}) | ||
// export default LocalFont; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import * as React from 'react'; | ||
import ThemeRegistry from '@/components/Theme/ThemeRegistry/ThemeRegistry'; | ||
|
||
export const metadata = { | ||
title: 'Next App with MUI5', | ||
description: 'next app with mui5', | ||
}; | ||
|
||
export default function RootLayout({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<html lang="en"> | ||
<body> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does MUI work with import {Inter} from 'next/font/google';
const inter = Inter({subsets: ['latin']});
// ...
<body className={inter.className}>
...
</body> If it does, would be really helpful to include it in the example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it works with the Next fonts, I can include it in the example. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
<ThemeRegistry>{children}</ThemeRegistry> | ||
</body> | ||
</html> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as React from 'react'; | ||
import Home from '@/layouts/Home/Home'; | ||
|
||
export default function RootPage() { | ||
return <Home />; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
import Typography from '@mui/material/Typography'; | ||
import Link from '@mui/material/Link'; | ||
|
||
export default function Copyright() { | ||
return ( | ||
<Typography variant="body2" color="text.secondary" align="center"> | ||
{'Copyright © '} | ||
<Link color="inherit" href="https://mui.com/"> | ||
Your Website | ||
</Link> | ||
{new Date().getFullYear()}. | ||
</Typography> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
import Typography from '@mui/material/Typography'; | ||
import Link from '@mui/material/Link'; | ||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; | ||
|
||
function LightBulbIcon(props: SvgIconProps) { | ||
return ( | ||
<SvgIcon {...props}> | ||
<path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z" /> | ||
</SvgIcon> | ||
); | ||
} | ||
|
||
export default function ProTip() { | ||
return ( | ||
<Typography sx={{ mt: 6, mb: 3 }} color="text.secondary"> | ||
<LightBulbIcon sx={{ mr: 1, verticalAlign: 'middle' }} /> | ||
Pro tip: See more <Link href="https://mui.com/getting-started/templates/">templates</Link> in | ||
the MUI documentation. | ||
</Typography> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,69 @@ | ||||||||
'use client'; | ||||||||
|
||||||||
import * as React from 'react'; | ||||||||
import createCache from '@emotion/cache'; | ||||||||
import { useServerInsertedHTML } from 'next/navigation'; | ||||||||
import { CacheProvider as DefaultCacheProvider } from '@emotion/react'; | ||||||||
import type { EmotionCache, Options as OptionsOfCreateCache } from '@emotion/cache'; | ||||||||
|
||||||||
export type NextAppDirEmotionCacheProviderProps = { | ||||||||
/** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */ | ||||||||
options: Omit<OptionsOfCreateCache, 'insertionPoint'>; | ||||||||
/** By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"' */ | ||||||||
CacheProvider?: (props: { | ||||||||
value: EmotionCache; | ||||||||
children: React.ReactNode; | ||||||||
}) => React.JSX.Element | null; | ||||||||
children: React.ReactNode; | ||||||||
}; | ||||||||
|
||||||||
// This implementation is taken from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. <3 |
||||||||
export function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Ideally, this should come from emotion. @Andarist would the emotion time accept this as a contribution? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think so. I have to give this some more thought but:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, we will use it for now in the example then, and I will keep an eye in the emotion issue. Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just want to clarify because I was perhaps misunderstood. Those were my general thoughts on the subject but I think that it would be valuable to get this into the Emotion's repo as an experimental package or smth. The advantage of that would be that we could deprecate this package in the future with a helpful note about the migration strategy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah alright, that makes sense. @garronej would you like to make a contribution as you originally created it? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also why it is needed/what it accomplishes. I see some related exposition from NextJS but don't think it is terribly clear either for novice learners: https://nextjs.org/docs/app/building-your-application/styling/css-in-js#configuring-css-in-js-in-app There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's an issue you can make a PR on this @sgoodrow-zymergen There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Hello, the provider ensures that when the app is rendered on the server, the styles generated by Emotion get injected into the Without the provider, Emotion defaults to its "zero config SSR mode", in which However, MUI is not compatible with Emotion's "zero config SSR mode". This has led to the recommended implementation with the Next Pages Router here being quite complex. Ever since MUI started utilizing Emotion, it has required the SSR advanced approach to be implemented, this provider is nothing new, it's the same old aprach implemented for the Next's App Router. A while back, I proposed an RFC suggesting an abstraction of this process. I've also initiated a PR on the Emotion repository to discuss transferring the TSS utilities. I'm absolutely prepared to contribute to this project. But as @Andarist pointed out, it would involve introducing a new experimental Emotion module, and he's better equipped to make the necessary decisions regarding that. |
||||||||
const { options, CacheProvider = DefaultCacheProvider, children } = props; | ||||||||
|
||||||||
const [{ cache, flush }] = React.useState(() => { | ||||||||
// eslint-disable-next-line @typescript-eslint/no-shadow | ||||||||
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); | ||||||||
}; | ||||||||
// eslint-disable-next-line @typescript-eslint/no-shadow | ||||||||
const flush = () => { | ||||||||
const prevInserted = inserted; | ||||||||
inserted = []; | ||||||||
return prevInserted; | ||||||||
}; | ||||||||
return { cache, flush }; | ||||||||
}); | ||||||||
|
||||||||
useServerInsertedHTML(() => { | ||||||||
const names = flush(); | ||||||||
if (names.length === 0) { | ||||||||
return null; | ||||||||
} | ||||||||
let styles = ''; | ||||||||
// eslint-disable-next-line no-restricted-syntax | ||||||||
for (const name of names) { | ||||||||
styles += cache.inserted[name]; | ||||||||
} | ||||||||
return ( | ||||||||
<style | ||||||||
key={cache.key} | ||||||||
data-emotion={`${cache.key} ${names.join(' ')}`} | ||||||||
// eslint-disable-next-line react/no-danger | ||||||||
dangerouslySetInnerHTML={{ | ||||||||
__html: styles, | ||||||||
}} | ||||||||
/> | ||||||||
); | ||||||||
}); | ||||||||
|
||||||||
return <CacheProvider value={cache}>{children}</CacheProvider>; | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
import { ThemeProvider } from '@mui/material/styles'; | ||
import CssBaseline from '@mui/material/CssBaseline'; | ||
import { NextAppDirEmotionCacheProvider } from '@/components/Theme/ThemeRegistry/EmotionCache'; | ||
import theme from './theme'; | ||
|
||
export default function ThemeRegistry({ children }: { children: React.ReactNode }) { | ||
return ( | ||
<NextAppDirEmotionCacheProvider options={{ key: 'mui' }}> | ||
<ThemeProvider theme={theme}> | ||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} | ||
<CssBaseline /> | ||
{children} | ||
</ThemeProvider> | ||
</NextAppDirEmotionCacheProvider> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
'use client'; | ||
|
||
import { createTheme } from '@mui/material/styles'; | ||
import GoogleInterFont from '@/app/fonts/fonts'; | ||
|
||
// When needed::: first argument is needed if you have common enterprise theme, and second argument is to override your enterprise theme. | ||
// apply fonts to all other typography options like headings, subtitles, etc... | ||
const defaultTheme = createTheme({ | ||
typography: { | ||
fontFamily: GoogleInterFont.style.fontFamily, | ||
body1: { fontFamily: GoogleInterFont.style.fontFamily }, | ||
body2: { fontFamily: GoogleInterFont.style.fontFamily }, | ||
}, | ||
}); | ||
|
||
export default defaultTheme; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
import Container from '@mui/material/Container'; | ||
import Typography from '@mui/material/Typography'; | ||
import Box from '@mui/material/Box'; | ||
import Link from 'next/link'; | ||
import ProTip from '@/components/ProTip/ProTip'; | ||
import Copyright from '@/components/CopyRight/Copyright'; | ||
|
||
export default function About() { | ||
return ( | ||
<Container maxWidth="lg"> | ||
<Box | ||
sx={{ | ||
my: 4, | ||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
}} | ||
> | ||
<Typography variant="h4" component="h1" gutterBottom> | ||
Material UI - Next.js example using App Router in TypeScript | ||
</Typography> | ||
<Link href="/">Go to the main page</Link> | ||
<ProTip /> | ||
<Copyright /> | ||
</Box> | ||
</Container> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
'use client'; | ||
|
||
import * as React from 'react'; | ||
import Container from '@mui/material/Container'; | ||
import Box from '@mui/material/Box'; | ||
import Typography from '@mui/material/Typography'; | ||
import Copyright from '@/components/CopyRight/Copyright'; | ||
import ProTip from '@/components/ProTip/ProTip'; | ||
import Link from 'next/link'; | ||
|
||
export default function Home() { | ||
return ( | ||
<Container maxWidth="lg"> | ||
<Box | ||
sx={{ | ||
my: 4, | ||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
}} | ||
> | ||
<Typography variant="h4" component="h1" gutterBottom> | ||
Material UI - Next.js example using App Router in TypeScript | ||
</Typography> | ||
<Link href="/about">Go to the about page</Link> | ||
<ProTip /> | ||
<Copyright /> | ||
</Box> | ||
</Container> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, great. I think that we really need to spend time on #35457.