diff --git a/.changeset/mean-sloths-wonder.md b/.changeset/mean-sloths-wonder.md new file mode 100644 index 0000000000..80cbb3bfab --- /dev/null +++ b/.changeset/mean-sloths-wonder.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/emotion': minor +--- + +This update exports a CacheProvider component for use in Next.JS applications diff --git a/packages/emotion/README.md b/packages/emotion/README.md index 065fba73a8..57683e8fa5 100644 --- a/packages/emotion/README.md +++ b/packages/emotion/README.md @@ -47,3 +47,244 @@ import App from './App'; const html = renderStylesToString(renderToString()); ``` + +## 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 ( + `; + const htmlWithStyles = html.replace('', `${emotionStyleTag}`); + + 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, + + + + + , + ); +}); +``` + +--- + +### 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/). diff --git a/packages/emotion/package.json b/packages/emotion/package.json index 65a431b2f0..df9004947a 100644 --- a/packages/emotion/package.json +++ b/packages/emotion/package.json @@ -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:^", diff --git a/packages/emotion/src/index.ts b/packages/emotion/src/index.ts index fa4f68eaec..aa0aa2ccc4 100644 --- a/packages/emotion/src/index.ts +++ b/packages/emotion/src/index.ts @@ -1,3 +1,4 @@ +import { CacheProvider } from '@emotion/react'; import createEmotionServer from '@emotion/server/create-instance'; import emotion from './emotion'; @@ -15,6 +16,8 @@ export const { cache, } = emotion; +export { CacheProvider }; + export const { extractCritical, renderStylesToString, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8c16ffb22..e82aeaa95b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -619,7 +619,7 @@ importers: devDependencies: '@emotion/styled': specifier: ^11.10.5 - version: 11.14.1(@emotion/react@11.11.1(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) '@lg-tools/build': specifier: workspace:^ version: link:../../tools/build @@ -1470,7 +1470,7 @@ importers: devDependencies: '@emotion/styled': specifier: ^11.10.5 - version: 11.14.1(@emotion/react@11.11.1(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) '@lg-tools/build': specifier: workspace:^ version: link:../../tools/build @@ -1802,6 +1802,9 @@ importers: '@emotion/css': specifier: ^11.1.3 version: 11.9.0(@babel/core@7.28.0) + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@18.2.23)(react@18.3.1) '@emotion/server': specifier: ^11.4.0 version: 11.11.0(@emotion/css@11.9.0(@babel/core@7.28.0)) @@ -2330,7 +2333,7 @@ importers: devDependencies: '@emotion/styled': specifier: ^11.10.5 - version: 11.14.1(@emotion/react@11.11.1(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) '@lg-tools/build': specifier: workspace:^ version: link:../../tools/build @@ -2746,7 +2749,7 @@ importers: devDependencies: '@emotion/styled': specifier: ^11.10.5 - version: 11.14.1(@emotion/react@11.11.1(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) '@lg-tools/build': specifier: workspace:^ version: link:../../tools/build @@ -3372,7 +3375,7 @@ importers: devDependencies: '@emotion/styled': specifier: ^11.14.0 - version: 11.14.1(@emotion/react@11.11.1(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1) '@faker-js/faker': specifier: ^8.0.0 version: 8.0.2 @@ -5542,6 +5545,9 @@ packages: '@emotion/cache@11.11.0': resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + '@emotion/css@11.9.0': resolution: {integrity: sha512-S9UjCxSrxEHawOLnWw4upTwfYKb0gVQdatHejn3W9kPyXxmKv3HmjVfJ84kDLmdX8jR20OuDQwaJ4Um24qD9vA==} peerDependencies: @@ -5577,6 +5583,15 @@ packages: '@types/react': optional: true + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + '@emotion/serialize@1.3.3': resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} @@ -5615,6 +5630,9 @@ packages: '@emotion/weak-memoize@0.3.1': resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@esbuild/aix-ppc64@0.25.8': resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} @@ -13741,6 +13759,14 @@ snapshots: '@emotion/weak-memoize': 0.3.1 stylis: 4.2.0 + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + '@emotion/css@11.9.0(@babel/core@7.24.3)': dependencies: '@emotion/babel-plugin': 11.11.0 @@ -13799,6 +13825,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@emotion/react@11.14.0(@types/react@18.2.23)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.23 + transitivePeerDependencies: + - supports-color + '@emotion/serialize@1.3.3': dependencies: '@emotion/hash': 0.9.2 @@ -13827,12 +13869,12 @@ snapshots: '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.11.1(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.2.23)(react@18.3.1))(@types/react@18.2.23)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.1 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 - '@emotion/react': 11.11.1(@types/react@18.2.23)(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.2.23)(react@18.3.1) '@emotion/serialize': 1.3.3 '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) '@emotion/utils': 1.4.2 @@ -13852,6 +13894,8 @@ snapshots: '@emotion/weak-memoize@0.3.1': {} + '@emotion/weak-memoize@0.4.0': {} + '@esbuild/aix-ppc64@0.25.8': optional: true