Skip to content

Commit 54d2f83

Browse files
authored
[LG-5798] feat: Emotion SSR Support (#3384)
* [LG-5798] feat: Emotion SSR Support * readme cleanup * more cleanup * added react router and gatsby,
1 parent f9fa5a3 commit 54d2f83

File tree

5 files changed

+302
-8
lines changed

5 files changed

+302
-8
lines changed

.changeset/mean-sloths-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/emotion': minor
3+
---
4+
5+
This update exports a CacheProvider component for use in Next.JS applications

packages/emotion/README.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,244 @@ import App from './App';
4747

4848
const html = renderStylesToString(renderToString(<App />));
4949
```
50+
51+
## SSR Compatibility
52+
53+
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).
54+
55+
> ⚠️ **Important:**
56+
>
57+
> - 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.
58+
> - Ensure you're using the latest version of any `emotion` packages alongside this package.
59+
> - LeafyGreen UI components may require additional configuration beyond what's documented here.
60+
61+
### Framework Guides
62+
63+
- [Next.js (App Router)](#nextjs-app-router)
64+
- [Next.js (Pages Router)](#nextjs-pages-router)
65+
- [React Router v7+](#react-router-v7)
66+
- [Gatsby.js](#gatsbyjs)
67+
68+
---
69+
70+
### Next.js (App Router)
71+
72+
#### 1. Create the Emotion Registry
73+
74+
Create a new file at `src/app/EmotionRegistry.tsx`:
75+
76+
```jsx
77+
'use client';
78+
79+
import { useServerInsertedHTML } from 'next/navigation';
80+
import { cache, CacheProvider } from '@leafygreen-ui/emotion';
81+
82+
export default function EmotionRegistry({
83+
children,
84+
}: {
85+
children: React.ReactNode,
86+
}) {
87+
useServerInsertedHTML(() => {
88+
const names = Object.keys(cache.inserted);
89+
if (names.length === 0) return null;
90+
91+
let styles = '';
92+
for (const name of names) {
93+
const style = cache.inserted[name];
94+
if (typeof style === 'string') {
95+
styles += style;
96+
}
97+
}
98+
99+
return (
100+
<style
101+
data-emotion={`${cache.key} ${names.join(' ')}`}
102+
dangerouslySetInnerHTML={{ __html: styles }}
103+
/>
104+
);
105+
});
106+
107+
return <CacheProvider value={cache}>{children}</CacheProvider>;
108+
}
109+
```
110+
111+
#### 2. Add the Registry to Your Root Layout
112+
113+
Wrap your application in `src/app/layout.tsx`:
114+
115+
```tsx
116+
import type { Metadata } from 'next';
117+
import EmotionRegistry from './EmotionRegistry';
118+
119+
export const metadata: Metadata = {
120+
title: 'My App',
121+
description: 'My application description',
122+
};
123+
124+
export default function RootLayout({
125+
children,
126+
}: Readonly<{
127+
children: React.ReactNode;
128+
}>) {
129+
return (
130+
<html lang="en">
131+
<body>
132+
<EmotionRegistry>{children}</EmotionRegistry>
133+
</body>
134+
</html>
135+
);
136+
}
137+
```
138+
139+
#### 3. Use in Client Components
140+
141+
> The `css` function only works in **Client Components**:
142+
143+
```tsx
144+
'use client';
145+
146+
import { css } from '@leafygreen-ui/emotion';
147+
148+
export default function MyComponent() {
149+
return (
150+
<h1
151+
className={css`
152+
color: red;
153+
`}
154+
>
155+
Hello World
156+
</h1>
157+
);
158+
}
159+
```
160+
161+
---
162+
163+
### Next.js (Pages Router)
164+
165+
Add Emotion's critical CSS extraction to your `_document` file:
166+
167+
```tsx
168+
import { extractCritical } from '@leafygreen-ui/emotion';
169+
170+
export default class AppDocument extends Document {
171+
static async getInitialProps(
172+
ctx: DocumentContext,
173+
): Promise<DocumentInitialProps> {
174+
const initialProps = await Document.getInitialProps(ctx);
175+
const { css, ids } = extractCritical(initialProps.html || '');
176+
177+
return {
178+
...initialProps,
179+
styles: (
180+
<>
181+
{initialProps.styles}
182+
<style
183+
data-emotion={`css ${ids.join(' ')}`}
184+
dangerouslySetInnerHTML={{ __html: css }}
185+
/>
186+
</>
187+
),
188+
};
189+
// ...
190+
}
191+
}
192+
```
193+
194+
---
195+
196+
### React Router v7+
197+
198+
This guide covers [Framework mode](https://reactrouter.com/start/modes#framework) for React Router.
199+
200+
#### 1. Configure Server Entry
201+
202+
```tsx
203+
Update `entry.server.tsx`:
204+
205+
import { PassThrough } from 'node:stream';
206+
import type { EntryContext } from 'react-router';
207+
import { createReadableStreamFromReadable } from '@react-router/node';
208+
import { ServerRouter } from 'react-router';
209+
import { renderToPipeableStream } from 'react-dom/server';
210+
import { cache, extractCritical, CacheProvider } from '@leafygreen-ui/emotion';
211+
212+
const ABORT_DELAY = 5_000;
213+
214+
export default function handleRequest(
215+
request: Request,
216+
responseStatusCode: number,
217+
responseHeaders: Headers,
218+
routerContext: EntryContext,
219+
) {
220+
return new Promise<Response>((resolve, reject) => {
221+
let statusCode = responseStatusCode;
222+
const chunks: Buffer[] = [];
223+
224+
const { pipe, abort } = renderToPipeableStream(
225+
<CacheProvider value={cache}>
226+
<ServerRouter context={routerContext} url={request.url} />
227+
</CacheProvider>,
228+
);
229+
230+
const collectStream = new PassThrough();
231+
collectStream.on('data', chunk => chunks.push(chunk));
232+
233+
collectStream.on('end', () => {
234+
const html = Buffer.concat(chunks).toString('utf-8');
235+
const { css, ids } = extractCritical(html);
236+
const emotionStyleTag = `<style data-emotion="css ${ids.join(' ')}">${css}</style>`;
237+
const htmlWithStyles = html.replace('</head>', `${emotionStyleTag}</head>`);
238+
239+
const body = new PassThrough();
240+
const stream = createReadableStreamFromReadable(body);
241+
242+
responseHeaders.set('Content-Type', 'text/html');
243+
resolve(
244+
new Response(stream, {
245+
headers: responseHeaders,
246+
status: statusCode,
247+
}),
248+
);
249+
250+
body.write(htmlWithStyles);
251+
body.end();
252+
});
253+
254+
collectStream.on('error', reject);
255+
pipe(collectStream);
256+
setTimeout(abort, ABORT_DELAY);
257+
258+
});
259+
}
260+
```
261+
262+
#### 2. Configure Client Entry
263+
264+
Update `entry.client.tsx`:
265+
266+
```tsx
267+
import { startTransition, StrictMode } from 'react';
268+
import { hydrateRoot } from 'react-dom/client';
269+
import { HydratedRouter } from 'react-router/dom';
270+
import { CacheProvider, cache } from '@leafygreen-ui/emotion';
271+
272+
startTransition(() => {
273+
hydrateRoot(
274+
document,
275+
<StrictMode>
276+
<CacheProvider value={cache}>
277+
<HydratedRouter />
278+
</CacheProvider>
279+
</StrictMode>,
280+
);
281+
});
282+
```
283+
284+
---
285+
286+
### Gatsby.js
287+
288+
> ⚠️ **Not Currently Supported**
289+
>
290+
> 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/).

packages/emotion/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
},
1919
"dependencies": {
2020
"@emotion/css": "^11.1.3",
21-
"@emotion/server": "^11.4.0"
21+
"@emotion/server": "^11.4.0",
22+
"@emotion/react": "^11.14.0"
2223
},
2324
"devDependencies": {
2425
"@lg-tools/build": "workspace:^",

packages/emotion/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CacheProvider } from '@emotion/react';
12
import createEmotionServer from '@emotion/server/create-instance';
23

34
import emotion from './emotion';
@@ -15,6 +16,8 @@ export const {
1516
cache,
1617
} = emotion;
1718

19+
export { CacheProvider };
20+
1821
export const {
1922
extractCritical,
2023
renderStylesToString,

0 commit comments

Comments
 (0)