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] Add Next.js 13 starter project #34983

Closed
wants to merge 12 commits into from
34 changes: 34 additions & 0 deletions examples/nextjs-13/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel
46 changes: 46 additions & 0 deletions examples/nextjs-13/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Next.js example

## 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/nextjs-13
cd nextjs
```

Install it and run:

```sh
npm install
npm run dev
```

or:

<!-- #default-branch-switch -->

[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mui/material-ui/tree/master/examples/nextjs-13)

[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/mui/material-ui/tree/master/examples/nextjs-13)

## 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`, the default style engine in MUI v5.
If you prefer, you can [use styled-components instead](https://mui.com/material-ui/guides/interoperability/#styled-components).
This examples uses the new experimental features of Next.js 13 - the app directory and client components.

## The link component

The [example folder](https://github.com/mui/material-ui/tree/HEAD/examples/nextjs-with-typescript) provides an adapter for the use of [Next.js's Link component](https://nextjs.org/docs/api-reference/next/link) with MUI.
More information [in the documentation](https://mui.com/material-ui/guides/routing/#next-js).

## 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.
33 changes: 33 additions & 0 deletions examples/nextjs-13/app/emotion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import * as React from 'react';
import PropTypes from 'prop-types';
import { CacheProvider } from '@emotion/react';
import { useServerInsertedHTML } from 'next/navigation';
import createEmotionCache from '../src/createEmotionCache';

export default function RootStyleRegistry({ children }) {
const [cache] = React.useState(() => {
const c = createEmotionCache();
c.compat = true;
return c;
});

useServerInsertedHTML(() => {
return (
<style
data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: Object.values(cache.inserted).join(' '),
}}
/>
);
});

return <CacheProvider value={cache}>{children}</CacheProvider>;
}

RootStyleRegistry.propTypes = {
children: PropTypes.node,
};
30 changes: 30 additions & 0 deletions examples/nextjs-13/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';
Copy link
Member Author

Choose a reason for hiding this comment

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

We shouldn't move forward with this PR before we are able to get rid of this directive. The layout shouldn't be a client component.

Copy link
Member

Choose a reason for hiding this comment

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

A couple of thoughts:

  1. Server-side component is not the only benefit of using Next.js's beta app folder. They list 4 in https://nextjs.org/blog/next-13#new-app-directory-beta:

Screenshot 2022-12-01 at 23 48 22

It could maybe make sense to provide a WIP demo. For example, to benefit from nested routes. Mantine as a WIP example mantinedev/mantine#2815 (comment). It could also resonate with https://mui.zendesk.com/agent/tickets/5883

we are hoping to use Next 13 with the app directory and layout.tsx files, to keep from having to retrofit the app after the fact

  1. We used the font logic to remove layout shifts in the examples [examples] Next.js examples v13 - fonts #34971. I guess we should do the same with our own docs?
  2. Server-side components can't use React.useContext, React.useState, React.useRef. So, let's say the issue is solve with emotion, we would also have to decide when the component needs to be client-side. There are cases, e.g. a button that could work server-side only even if it has a ton of hooks inside. So maybe, we will need to have a custom React.useState, React.useRef utils that do a no-op for these cases. https://twitter.com/olivtassinari/status/1601884631426633728
  3. Next.js not supporting the context for the server-side components doesn't make sense to me. Can I use Server Component with a global Context? vercel/next.js#42301 cover a bit of the problem. It feels like it was created in a bubble, without considering how real-life app constraints are. For a given request, a theme is static, a locale is static, etc.

Copy link

@madaher-dev madaher-dev Dec 2, 2022

Choose a reason for hiding this comment

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

It feels like it was created in a bubble, without considering how real-life app constraints are. For a given request, a theme is static, a locale is static, etc.

Exactly!

Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't move forward with this PR before we are able to get rid of this directive. The layout shouldn't be a client component.

Per #34905 (comment), I think that it would be great to merge to have this demo, even if with use client. The /app folder has more to offer beyond server-side components.


import * as React from 'react';
import PropTypes from 'prop-types';
import RootStyleRegistry from './emotion';
import theme, { roboto } from '../src/theme';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';

export default function RootLayout({ children }) {
return (
<html lang="en" className={roboto.className}>
<head>
<meta name="emotion-insertion-point" content="" />
</head>
<body>
<RootStyleRegistry>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</RootStyleRegistry>
</body>
</html>
);
}

RootLayout.propTypes = {
children: PropTypes.node,
};
27 changes: 27 additions & 0 deletions examples/nextjs-13/app/next13/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'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 Button from '@mui/material/Button';
import ProTip from '../../src/ProTip';
import Link from '../../src/Link';
import Copyright from '../../src/Copyright';

export default function About() {
return (
<Container maxWidth="sm">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Next.js app page example
</Typography>
<Button variant="outlined" component={Link} noLinkStyle href="/">
Go to the main page
</Button>
<ProTip />
<Copyright />
</Box>
</Container>
);
}
6 changes: 6 additions & 0 deletions examples/nextjs-13/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
reactStrictMode: true,
experimental: {
appDir: true,
},
};
24 changes: 24 additions & 0 deletions examples/nextjs-13/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "nextjs-13",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "latest",
"@emotion/server": "latest",
"@emotion/styled": "latest",
"@mui/material": "latest",
"@next/font": "latest",
"next": "latest",
"react": "latest",
"react-dom": "latest"
},
"devDependencies": {
"eslint": "latest",
"eslint-config-next": "latest"
}
}
34 changes: 34 additions & 0 deletions examples/nextjs-13/pages/_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider } from '@emotion/react';
import theme from '../src/theme';
import createEmotionCache from '../src/createEmotionCache';

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

export default function MyApp(props) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;

return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
);
}

MyApp.propTypes = {
Component: PropTypes.elementType.isRequired,
emotionCache: PropTypes.object,
pageProps: PropTypes.object.isRequired,
};
84 changes: 84 additions & 0 deletions examples/nextjs-13/pages/_document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import theme, { roboto } from '../src/theme';
import createEmotionCache from '../src/createEmotionCache';

export default class MyDocument extends Document {
render() {
return (
<Html lang="en" className={roboto.className}>
<Head>
{/* PWA primary color */}
<meta name="theme-color" content={theme.palette.primary.main} />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="emotion-insertion-point" content="" />
{this.props.emotionStyleTags}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render

const originalRenderPage = ctx.renderPage;

// You can consider sharing the same Emotion cache between all the SSR requests to speed up performance.
// However, be aware that it can have global side effects.
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);

ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) =>
function EnhanceApp(props) {
return <App emotionCache={cache} {...props} />;
},
});

const initialProps = await Document.getInitialProps(ctx);
// This is important. It prevents Emotion to render invalid HTML.
// See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153
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,
};
};
25 changes: 25 additions & 0 deletions examples/nextjs-13/pages/about.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import ProTip from '../src/ProTip';
import Link from '../src/Link';
import Copyright from '../src/Copyright';

export default function About() {
return (
<Container maxWidth="sm">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Next.js example
</Typography>
<Button variant="outlined" component={Link} noLinkStyle href="/">
Go to the main page
</Button>
<ProTip />
<Copyright />
</Box>
</Container>
);
}
5 changes: 5 additions & 0 deletions examples/nextjs-13/pages/api/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' });
}
30 changes: 30 additions & 0 deletions examples/nextjs-13/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import ProTip from '../src/ProTip';
import Link from '../src/Link';
import Copyright from '../src/Copyright';

export default function Index() {
return (
<Container maxWidth="sm">
<Box sx={{ my: 4 }}>
<Typography variant="h4" component="h1" gutterBottom>
Next.js example
</Typography>
<Stack gap={1}>
<Link href="/about" color="secondary">
Go to the about page
</Link>
<Link href="/next13" color="secondary">
Go to an app page
</Link>
</Stack>
<ProTip />
<Copyright />
</Box>
</Container>
);
}
Binary file added examples/nextjs-13/public/favicon.ico
Binary file not shown.
4 changes: 4 additions & 0 deletions examples/nextjs-13/public/vercel.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.