Server-Side Rendering
Griffel provides first class support for Server-Side Rendering.
Next.js (Pages Router)โ
Base setupโ
For basic instructions to setup Next.js, see Getting Started. Please complete the following steps:
- Get a basic Next.js setup running, rendering a page from the
pagesfolder, as guided by the tutorial. - Add the Griffel to dependencies (
@griffel/reactpackage), check Install page.
A complete demo project is available on CodeSandbox.
Configuring a projectโ
- Create a
_document.jsfile under yourpagesfolder with the following content:
import { createDOMRenderer, renderToStyleElements } from '@griffel/react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
static async getInitialProps(ctx) {
// ๐ creates a renderer
const renderer = createDOMRenderer();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => <App {...props} renderer={renderer} />,
});
const initialProps = await Document.getInitialProps(ctx);
const styles = renderToStyleElements(renderer);
return {
...initialProps,
// ๐ adding our styles elements to output
styles: [...initialProps.styles, ...styles],
};
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
- Create or modify an
_app.jsfile under yourpagesfolder with the following content:
import { createDOMRenderer, RendererProvider } from '@griffel/react';
function MyApp({ Component, pageProps, renderer }) {
return (
// ๐ accepts a renderer passed from the <Document /> component or creates a default one
<RendererProvider renderer={renderer || createDOMRenderer()}>
<Component {...pageProps} />
</RendererProvider>
);
}
export default MyApp;
- You should now be able to server render components with Griffel styles on any of your pages:
import { makeStyles } from '@griffel/react';
const useClasses = makeStyles({
button: { fontWeight: 'bold' },
});
export default function Home() {
const classes = useClasses();
return <Button className={classes.button}>Hello world!</Button>;
}
Next.js (App Router)โ
The App Router streams HTML, so styles are flushed with useServerInsertedHTML instead of a custom _document.
Configuring a projectโ
- Create a Client Component that owns the renderer and flushes its styles:
'use client';
import { createDOMRenderer, RendererProvider, renderToStyleElements } from '@griffel/react';
import { useServerInsertedHTML } from 'next/navigation';
import { useRef, useState } from 'react';
export function GriffelRegistry({ children }: { children: React.ReactNode }) {
// ๐ one renderer per request, kept stable across re-renders
const [renderer] = useState(() => createDOMRenderer());
const didRenderRef = useRef(false);
useServerInsertedHTML(() => {
// ๐ Flush the collected styles exactly once. Next calls this callback once per
// streaming flush, and renderToStyleElements() returns the renderer's ENTIRE CSS
// every time โ flushing on every call would duplicate it into <body> (see
// "Flush the styles only once" below).
if (didRenderRef.current) {
return;
}
didRenderRef.current = true;
return <>{renderToStyleElements(renderer)}</>;
});
return <RendererProvider renderer={renderer}>{children}</RendererProvider>;
}
- Wrap your app with it in
app/layout.tsx:
import { GriffelRegistry } from './griffel-registry';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<GriffelRegistry>{children}</GriffelRegistry>
</body>
</html>
);
}
- You can now server render components with Griffel styles anywhere in the tree (Client Components, since
makeStylesruns on the client renderer after hydration).
Flush the styles only onceโ
renderToStyleElements returns all the CSS the renderer has collected so far. In the Pages Router it runs once, so this is fine. In the App Router, Next calls useServerInsertedHTML once per streaming flush โ so without the didRenderRef guard, every flush re-emits the full stylesheet and the duplicate copies are streamed into <body>.
Those stale <body> copies persist across a client-side navigation and, sitting after <head> at equal specificity, can override the runtime styles Griffel inserts into <head> afterwards โ making makeStyles overrides lose to their makeResetStyles base (controls render at their default size, but only after a soft navigation; a hard reload looks correct).
The didRenderRef guard emits the collected styles on the first flush and skips the rest, so nothing is duplicated. This is the same setup Fluent UI โ which is built on Griffel โ uses for the App Router.
To verify, request a page without JavaScript and confirm the data-make-styles-rehydration markers appear only in <head>, never in <body>.