Skip to content
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

Merged
merged 9 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions examples/material-next-app-router-ts/.gitignore
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
31 changes: 31 additions & 0 deletions examples/material-next-app-router-ts/README.md
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.
5 changes: 5 additions & 0 deletions examples/material-next-app-router-ts/next-env.d.ts
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.
12 changes: 12 additions & 0 deletions examples/material-next-app-router-ts/next.config.js
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}}',
},
},
Comment on lines +5 to +9
Copy link
Member

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.

};

module.exports = nextConfig;
30 changes: 30 additions & 0 deletions examples/material-next-app-router-ts/package.json
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"
}
}
Empty file.
6 changes: 6 additions & 0 deletions examples/material-next-app-router-ts/src/app/about/page.tsx
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 />;
}
Binary file not shown.
12 changes: 12 additions & 0 deletions examples/material-next-app-router-ts/src/app/fonts/fonts.ts
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;
17 changes: 17 additions & 0 deletions examples/material-next-app-router-ts/src/app/layout.tsx
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>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does MUI work with next/font?

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

@smo043 smo043 May 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with next/fonts: Next.js automatically generates a styles file and inserts the fonts reference to it.

font_styles font

<ThemeRegistry>{children}</ThemeRegistry>
</body>
</html>
);
}
6 changes: 6 additions & 0 deletions examples/material-next-app-router-ts/src/app/page.tsx
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

export function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) {
// This implementation is taken from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx
export function NextAppDirEmotionCacheProvider(props: NextAppDirEmotionCacheProviderProps) {

Ideally, this should come from emotion. @Andarist would the emotion time accept this as a contribution?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. I have to give this some more thought but:

  • this is somewhat working and this solution is shared in our issues so everyone can copy-paste it anyway
  • it likely has some subtle/edge-case issues but people usually don't run into them at all so I think it's roughly OK to be used right now
  • it's a temporary solution - so it would have to be a separate package. We are still waiting on improved React APIs that, hopefully, will make this obsolete

Copy link
Member

Choose a reason for hiding this comment

The 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!

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if @garronej or @Andarist can share a summary of how this work with app router.

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

@garronej garronej Jun 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if @garronej or @Andarist can share a summary of how this work with app router.

Hello, the provider ensures that when the app is rendered on the server, the styles generated by Emotion get injected into the <head> element.

Without the provider, Emotion defaults to its "zero config SSR mode", in which <style /> tags are injected into the body of the document, right next to the element being styled.

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.

@mnajdova @siriwatknp @sgoodrow-zymergen

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;
32 changes: 32 additions & 0 deletions examples/material-next-app-router-ts/src/layouts/About/About.tsx
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>
);
}
32 changes: 32 additions & 0 deletions examples/material-next-app-router-ts/src/layouts/Home/Home.tsx
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>
);
}
Loading