Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-sloths-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/emotion': minor
---

This update exports a CacheProvider component for use in Next.JS applications
241 changes: 241 additions & 0 deletions packages/emotion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,244 @@ import App from './App';

const html = renderStylesToString(renderToString(<App />));
```

## SSR Compatibility

Emotion generates styles at runtime and injects them into the DOM. For server-side rendering, styles must be extracted during rendering and inserted into the HTML before it's sent to the client. Without proper configuration, you'll see a flash of unstyled content (FOUC).

> ⚠️ **Important:**
>
> - Emotion does not [currently support React Server Components](https://github.com/emotion-js/emotion/issues/2978). You must use `'use client'` directive in Next.js.
> - Ensure you're using the latest version of any `emotion` packages alongside this package.
> - LeafyGreen UI components may require additional configuration beyond what's documented here.

### Framework Guides

- [Next.js (App Router)](#nextjs-app-router)
- [Next.js (Pages Router)](#nextjs-pages-router)
- [React Router v7+](#react-router-v7)
- [Gatsby.js](#gatsbyjs)

---

### Next.js (App Router)

#### 1. Create the Emotion Registry

Create a new file at `src/app/EmotionRegistry.tsx`:

```jsx
'use client';

import { useServerInsertedHTML } from 'next/navigation';
import { cache, CacheProvider } from '@leafygreen-ui/emotion';

export default function EmotionRegistry({
children,
}: {
children: React.ReactNode,
}) {
useServerInsertedHTML(() => {
const names = Object.keys(cache.inserted);
if (names.length === 0) return null;

let styles = '';
for (const name of names) {
const style = cache.inserted[name];
if (typeof style === 'string') {
styles += style;
}
}

return (
<style
data-emotion={`${cache.key} ${names.join(' ')}`}
dangerouslySetInnerHTML={{ __html: styles }}
/>
);
});

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

#### 2. Add the Registry to Your Root Layout

Wrap your application in `src/app/layout.tsx`:

```tsx
import type { Metadata } from 'next';
import EmotionRegistry from './EmotionRegistry';

export const metadata: Metadata = {
title: 'My App',
description: 'My application description',
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<EmotionRegistry>{children}</EmotionRegistry>
</body>
</html>
);
}
```

#### 3. Use in Client Components

> The `css` function only works in **Client Components**:

```tsx
'use client';

import { css } from '@leafygreen-ui/emotion';

export default function MyComponent() {
return (
<h1
className={css`
color: red;
`}
>
Hello World
</h1>
);
}
```

---

### Next.js (Pages Router)

Add Emotion's critical CSS extraction to your `_document` file:

```tsx
import { extractCritical } from '@leafygreen-ui/emotion';

export default class AppDocument extends Document {
static async getInitialProps(
ctx: DocumentContext,
): Promise<DocumentInitialProps> {
const initialProps = await Document.getInitialProps(ctx);
const { css, ids } = extractCritical(initialProps.html || '');

return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style
data-emotion={`css ${ids.join(' ')}`}
dangerouslySetInnerHTML={{ __html: css }}
/>
</>
),
};
// ...
}
}
```

---

### React Router v7+

This guide covers [Framework mode](https://reactrouter.com/start/modes#framework) for React Router.

#### 1. Configure Server Entry

```tsx
Update `entry.server.tsx`:

import { PassThrough } from 'node:stream';
import type { EntryContext } from 'react-router';
import { createReadableStreamFromReadable } from '@react-router/node';
import { ServerRouter } from 'react-router';
import { renderToPipeableStream } from 'react-dom/server';
import { cache, extractCritical, CacheProvider } from '@leafygreen-ui/emotion';

const ABORT_DELAY = 5_000;

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
) {
return new Promise<Response>((resolve, reject) => {
let statusCode = responseStatusCode;
const chunks: Buffer[] = [];

const { pipe, abort } = renderToPipeableStream(
<CacheProvider value={cache}>
<ServerRouter context={routerContext} url={request.url} />
</CacheProvider>,
);

const collectStream = new PassThrough();
collectStream.on('data', chunk => chunks.push(chunk));

collectStream.on('end', () => {
const html = Buffer.concat(chunks).toString('utf-8');
const { css, ids } = extractCritical(html);
const emotionStyleTag = `<style data-emotion="css ${ids.join(' ')}">${css}</style>`;
const htmlWithStyles = html.replace('</head>', `${emotionStyleTag}</head>`);

const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');
resolve(
new Response(stream, {
headers: responseHeaders,
status: statusCode,
}),
);

body.write(htmlWithStyles);
body.end();
});

collectStream.on('error', reject);
pipe(collectStream);
setTimeout(abort, ABORT_DELAY);

});
}
```

#### 2. Configure Client Entry

Update `entry.client.tsx`:

```tsx
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { HydratedRouter } from 'react-router/dom';
import { CacheProvider, cache } from '@leafygreen-ui/emotion';

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<CacheProvider value={cache}>
<HydratedRouter />
</CacheProvider>
</StrictMode>,
);
});
```

---

### Gatsby.js

> ⚠️ **Not Currently Supported**
>
> There is a peer dependency mismatch between `@leafygreen-ui/emotion` and `gatsby-plugin-emotion`. As a result, we do not currently support GatsbyJS projects out of the box. If you need Emotion in a Gatsby project, refer to the [Gatsby Emotion documentation](https://www.gatsbyjs.com/docs/how-to/styling/emotion/).
3 changes: 2 additions & 1 deletion packages/emotion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
},
"dependencies": {
"@emotion/css": "^11.1.3",
"@emotion/server": "^11.4.0"
"@emotion/server": "^11.4.0",
"@emotion/react": "^11.14.0"
},
"devDependencies": {
"@lg-tools/build": "workspace:^",
Expand Down
3 changes: 3 additions & 0 deletions packages/emotion/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';

import emotion from './emotion';
Expand All @@ -15,6 +16,8 @@ export const {
cache,
} = emotion;

export { CacheProvider };

export const {
extractCritical,
renderStylesToString,
Expand Down
Loading
Loading