Learn how to use Joy UI with the Next.js App Router.
Starting fresh on a new App Router-based project?
Jump right into the code with this example: Joy UI - Next.js App Router with TypeScript.
The Next.js App Router implements React Server Components, an upcoming feature for React.
To support the App Router, the components and hooks from Joy UI that need access to browser APIs 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 of Client Components and SSR from the React Working Group. :::
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:
// 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(props) {
return (
<html lang="en">
<body>
<ThemeRegistry options={{ key: 'joy' }}>{props.children}</ThemeRegistry>
</body>
</html>
);
}
Props passed from server components-for example page.js
or other routing files-must be serializable.
:::success This works without any additional directives:
// app/page.tsx
import Sheet from '@mui/joy/Sheet';
import Typography from '@mui/joy/Typography';
export default function Page() {
return (
<Sheet>
<Typography fontSize="sm">Hello World</Typography>
</Sheet>
);
}
:::
:::error This doesn't work because the Button's click handler is non-serializable:
// app/page.tsx
import Button from '@mui/joy/Button';
import Sheet from '@mui/joy/Sheet';
export default function Page() {
return (
<Sheet>
{/* Next.js won't render this button without 'use-client' */}
<Button
onClick={() => {
console.log('handle click');
}}
>
Submit
</Button>
</Sheet>
);
}
Instead, the Next.js team recommend moving components like these "down the tree" to avoid this issue and improve overall performance. :::