-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[core] Prepend "use-client" directive + add docs and examples for usi…
…ng MUI libraries with Next.js App Router (#37656)
- Loading branch information
1 parent
0bfdfb4
commit a4afa9f
Showing
702 changed files
with
2,686 additions
and
165 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
198 changes: 198 additions & 0 deletions
198
docs/data/base/guides/next-js-app-router/next-js-app-router.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
# Next.js App Router | ||
|
||
<p class="description">Learn how to use Base UI with the Next.js App Router.</p> | ||
|
||
:::info | ||
Starting fresh on a new App Router-based project? | ||
|
||
Jump right into the code with this [example repo](https://github.com/mui/material-ui/blob/master/examples/base-next-app-router-ts). | ||
::: | ||
|
||
## Next.js and React Server Components | ||
|
||
The Next.js App Router implements React Server Components, a [new feature](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#changes-since-v1) introduced in React 18. | ||
|
||
To support the App Router, currently all components and hooks from Base UI and other MUI libraries are exported with the `"use client"` directive. | ||
|
||
:::warning | ||
React Server Components should not be conflated with the concept of server-side rendering (SSR). | ||
So-called Client Components are still server-rendered to HTML. | ||
|
||
For more details, see [this explanation](https://github.com/reactwg/server-components/discussions/4) of Client Components and SSR from the React Working Group. | ||
::: | ||
|
||
## Setting up Base UI with the App Router | ||
|
||
Base UI gives you the freedom to choose your own styling solution, so setting up a Next.js App Router project largely depends on what you choose. | ||
This guide covers Tailwind CSS, Emotion, and other CSS-in-JS solutions like styled-components. | ||
|
||
### Tailwind CSS | ||
|
||
Follow the [Tailwind CSS guide on working with Next.js](https://tailwindcss.com/docs/guides/nextjs), and be sure to add the `app` directory and other directories to `tailwind.config.js`, as shown below: | ||
|
||
```js | ||
/** @type {import('tailwindcss').Config} */ | ||
module.exports = { | ||
content: [ | ||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}', | ||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}' | ||
|
||
// or if not using the `src` directory: | ||
'./app/**/*.{js,ts,jsx,tsx,mdx}', | ||
], | ||
theme: { | ||
extend: {}, | ||
}, | ||
plugins: [], | ||
}; | ||
``` | ||
|
||
Refer to this [example repo](https://github.com/mui/material-ui/blob/master/examples/base-next-app-router-tailwind-ts) for a full working demo of a Next.js 13 app using Base UI and Tailwind CSS. | ||
|
||
### Emotion | ||
|
||
If you're using Emotion, or something Emotion-based like MUI System, create a custom `ThemeRegistry` component that combines the Emotion `CacheProvider`, the Material UI `ThemeProvider`, and the `useServerInsertedHTML` hook from `next/navigation` as follows: | ||
|
||
```tsx | ||
// app/ThemeRegistry.tsx | ||
'use client'; | ||
import createCache from '@emotion/cache'; | ||
import { useServerInsertedHTML } from 'next/navigation'; | ||
import { CacheProvider, ThemeProvider } from '@emotion/react'; | ||
import theme from '/path/to/your/theme'; | ||
|
||
// This implementation is from emotion-js | ||
// https://github.com/emotion-js/emotion/issues/2928#issuecomment-1319747902 | ||
export default function ThemeRegistry(props) { | ||
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}>{children}</ThemeProvider> | ||
</CacheProvider> | ||
); | ||
} | ||
|
||
// app/layout.js | ||
export default function RootLayout({ children }) { | ||
return ( | ||
<html lang="en"> | ||
<body> | ||
<ThemeRegistry options={{ key: 'mui' }}>{children}</ThemeRegistry> | ||
</body> | ||
</html> | ||
); | ||
} | ||
``` | ||
|
||
If you need to further override theme styles (e.g. using CSS modules), Emotion provides the `prepend: true` option for `createCache` to reverse the injection order, so custom styles can override the theme without using `!important`. | ||
|
||
Currently, `prepend` does not work reliably with the App Router, but you can work around it by wrapping Emotion styles in a CSS `@layer` with a modification to the snippet above: | ||
|
||
```diff | ||
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, | ||
+ __html: options.prepend ? `@layer emotion {${styles}}` : styles, | ||
}} | ||
/> | ||
); | ||
}); | ||
``` | ||
|
||
### Other CSS-in-JS libraries | ||
|
||
To use Next.js with Base UI and styled-components or other CSS-in-JS solutions, follow the [Next.js doc on CSS-in-JS](https://nextjs.org/docs/app/building-your-application/styling/css-in-js). | ||
|
||
## Customization | ||
|
||
### Using callbacks for slot props | ||
|
||
A common customization method in Base UI is to pass a callback to slots in `slotProps` in order to apply dynamic props. For example, you might want to change the background color by applying a different class when a Button is disabled: | ||
|
||
```tsx | ||
// page.tsx | ||
|
||
export default function Page() { | ||
return ( | ||
<> | ||
{/* Next.js won't render this button without 'use-client'*/} | ||
<Button | ||
slotProps={{ | ||
root: (ownerState: ButtonOwnerState) => ({ | ||
className: ownerState.disabled ? 'bg-gray-400' : 'bg-blue-400', | ||
}), | ||
}} | ||
> | ||
Submit | ||
</Button> | ||
|
||
{/* Next.js can render this */} | ||
<Button | ||
slotProps={{ | ||
root: { | ||
className: 'bg-gray-400', | ||
}, | ||
}} | ||
> | ||
Return | ||
</Button> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
Unfortunately, **this does not work in a Server Component** since function props are [non-serializable](https://nextjs.org/docs/getting-started/react-essentials#passing-props-from-server-to-client-components-serialization). | ||
Instead, the Next.js team recommend moving components like these ["to the leaves"](https://nextjs.org/docs/getting-started/react-essentials#moving-client-components-to-the-leaves) to avoid this issue and improve overall performance. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
docs/data/joy/guides/next-js-app-router/next-js-app-router.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
# Next.js App Router | ||
|
||
<p class="description">Learn how to use Joy UI with the Next.js App Router.</p> | ||
|
||
:::info | ||
Starting fresh on a new App Router-based project? | ||
|
||
Jump right into the code with this [example repo](https://github.com/mui/material-ui/blob/master/examples/joy-next-app-router-ts). | ||
::: | ||
|
||
## Next.js and React Server Components | ||
|
||
The Next.js App Router implements React Server Components, a [new feature](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md#changes-since-v1) introduced in React 18. | ||
|
||
To support the App Router, currently all components and hooks from Joy UI and other MUI libraries are exported with the `"use client"` directive. | ||
|
||
:::warning | ||
React Server Components should not be conflated with the concept of server-side rendering (SSR). | ||
So-called Client Components are still server-rendered to HTML. | ||
|
||
For more details, see [this explanation](https://github.com/reactwg/server-components/discussions/4) of Client Components and SSR from the React Working Group. | ||
::: | ||
|
||
## Using Joy UI with the App Router | ||
|
||
To set up Joy UI, create a custom `ThemeRegistry` component that combines the Emotion `CacheProvider`, Joy UI's `CssVarsProvider` and the `useServerInsertedHTML` hook from `next/navigation` as follows: | ||
|
||
```tsx | ||
// app/ThemeRegistry.tsx | ||
'use client'; | ||
import createCache from '@emotion/cache'; | ||
import { useServerInsertedHTML } from 'next/navigation'; | ||
import { CacheProvider } from '@emotion/react'; | ||
import { CssVarsProvider } from '@mui/joy/styles'; | ||
import CssBaseline from '@mui/joy/CssBaseline'; | ||
import theme from '/path/to/custom/theme'; // OPTIONAL | ||
|
||
// This implementation is from emotion-js | ||
// https://github.com/emotion-js/emotion/issues/2928#issuecomment-1319747902 | ||
export default function ThemeRegistry(props) { | ||
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}> | ||
<CssVarsProvider theme={theme}> | ||
{/* the custom theme is optional */} | ||
<CssBaseline /> | ||
{children} | ||
</CssVarsProvider> | ||
</CacheProvider> | ||
); | ||
} | ||
|
||
// app/layout.tsx | ||
export default function RootLayout({ children }) { | ||
return ( | ||
<html lang="en"> | ||
<body> | ||
<ThemeRegistry options={{ key: 'joy' }}>{children}</ThemeRegistry> | ||
</body> | ||
</html> | ||
); | ||
} | ||
``` | ||
|
||
## Function props | ||
|
||
Props passed from server components—for example `page.js` or other routing files—must be [serializable](https://nextjs.org/docs/getting-started/react-essentials#passing-props-from-server-to-client-components-serialization). | ||
|
||
This works without any additional directives: | ||
|
||
```jsx | ||
// app/page.tsx | ||
import Sheet from '@mui/joy/Sheet'; | ||
import Typography from '@mui/joy/Typography'; | ||
|
||
export default function Page() { | ||
return ( | ||
<> | ||
<Sheet variant="outlined"> | ||
<Typography fontSize="sm">Hello World</Typography> | ||
</Sheet> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
:::error | ||
🚨 This code snippet _doesn't work_, because the Button's click handler is **non-serializable**: | ||
|
||
```tsx | ||
// page.tsx | ||
import Button from '@mui/joy/Button'; | ||
import Sheet from '@mui/joy/Sheet'; | ||
|
||
export default function Page() { | ||
return ( | ||
<> | ||
<Sheet variant="outlined"> | ||
{/* Next.js won't render this button without 'use-client' */} | ||
<Button | ||
variant="outlined" | ||
onClick={() => { | ||
console.log('handle click'); | ||
}} | ||
> | ||
Submit | ||
</Button> | ||
</Sheet> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
Instead, the Next.js team recommend moving components like these ["to the leaves"](https://nextjs.org/docs/getting-started/react-essentials#moving-client-components-to-the-leaves) to avoid this issue and improve overall performance. | ||
::: |
Oops, something went wrong.