Skip to content

Commit

Permalink
[core] Prepend "use-client" directive + add docs and examples for usi…
Browse files Browse the repository at this point in the history
…ng MUI libraries with Next.js App Router (#37656)
  • Loading branch information
mj12albert authored Jul 11, 2023
1 parent 0bfdfb4 commit a4afa9f
Show file tree
Hide file tree
Showing 702 changed files with 2,686 additions and 165 deletions.
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ jobs:
- run:
name: '`yarn proptypes` changes committed?'
command: git diff --exit-code
- run:
name: '`yarn rsc:build` changes committed?'
command: git diff --exit-code
- run:
name: Generate the documentation
command: yarn docs:api
Expand Down
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ module.exports = {
'react/no-invalid-html-attribute': 'off',

'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
'lines-around-directive': 'off',
},
overrides: [
{
Expand Down
4 changes: 4 additions & 0 deletions docs/data/base/getting-started/quickstart/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

<p class="description">Get started with Base UI, a library of headless ("unstyled") React UI components and low-level hooks.</p>

:::info
If you're using Next.js 13.4 or later, check out the [Next.js App Router guide](/base-ui/guides/next-js-app-router/)
:::

## Installation

`@mui/base` is completely standalone – run one of the following commands to add Base UI to your React project:
Expand Down
198 changes: 198 additions & 0 deletions docs/data/base/guides/next-js-app-router/next-js-app-router.md
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.
4 changes: 4 additions & 0 deletions docs/data/base/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ const pages = [
pathname: '/base-ui/guides/overriding-component-structure',
title: 'Overriding component structure',
},
{
pathname: '/base-ui/guides/next-js-app-router',
title: 'Integrating with Next.js App Router',
},
],
},
];
Expand Down
156 changes: 156 additions & 0 deletions docs/data/joy/guides/next-js-app-router/next-js-app-router.md
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.
:::
Loading

0 comments on commit a4afa9f

Please sign in to comment.