πŸ‡ΊπŸ‡¦ STOP WAR IN UKRAINE πŸ‡ΊπŸ‡¦
Avatar

Emotion

Server Side Rendering

✏️ Edit this page

Server side rendering in Emotion 10 has two approaches, each with their own trade-offs. The default approach works with streaming and requires no additional configuration, but does not work with nth child or similar selectors. It's strongly recommended that you use the default approach unless you need nth child or similar selectors.

Default Approach

Server side rendering works out of the box in Emotion 10 and above if you're only using @emotion/react and @emotion/styled. This means you can call React's renderToString or renderToNodeStream methods directly without any extra configuration.

import { renderToString } from 'react-dom/server'
import App from './App'

let html = renderToString(<App />)

The rendered output will insert a <style> tag above each element with styles for example

const MyDiv = styled('div')({ fontSize: 12 })
<MyDiv>Text</MyDiv>
// Will render as
<style data-emotion-css="21cs4">.css-21cs4 { font-size: 12 }</style>
<div class="css-21cs4">Text</div>

Warning: This approach can interfere with nth child and similar selectors as it inserts style tags directly into your markup. You will get a warning if you use such selectors when using this approach.

Advanced Approach

Note: If you're not using nth child or similar selectors, you don't need to do this. Use the default approach.

You can also use the advanced integration, it requires more work but does not have limitations on nth child and similar selectors. This approach does not work with the streaming APIs.

On server

When using @emotion/react

import { CacheProvider } from '@emotion/react'
import { renderToString } from 'react-dom/server'
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'

const key = 'custom'
const cache = createCache({ key })
const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache)

const html = renderToString(
  <CacheProvider value={cache}>
    <App />
  </CacheProvider>
)

const chunks = extractCriticalToChunks(html)
const styles = constructStyleTagsFromChunks(chunks)

res
  .status(200)
  .header('Content-Type', 'text/html')
  .send(`<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My site</title>
    ${styles}
</head>
<body>
    <div id="root">${html}</div>

    <script src="./bundle.js"></script>
</body>
</html>`);

When using @emotion/css

import { CacheProvider } from '@emotion/react'
import { renderToString } from 'react-dom/server'
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'

const key = 'custom'
const cache = createCache({ key })
const { extractCritical } = createEmotionServer(cache)

let element = (
  <CacheProvider value={cache}>
    <App />
  </CacheProvider>
)

let { html, css, ids } = extractCritical(renderToString(element))

res
  .status(200)
  .header('Content-Type', 'text/html')
  .send(`<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>My site</title>
    <style data-emotion="${key} ${ids.join(' ')}">${css}</style>
</head>
<body>
    <div id="root">${html}</div>

    <script src="./bundle.js"></script>
</body>
</html>`);

On client

// Hydration of the ids in `data-emotion-css` will automatically occur when the cache is created
const cache = createCache()
ReactDOM.hydrate(
  <CacheProvider value={cache}>
    <App />
  </CacheProvider>,
  document.getElementById('root')
)

In this approach you have to create your own cache and emotion server then use extractCritical

API Reference

renderStylesToString

This returns a string of html that inlines the critical css required right before it's used.

import { renderToString } from 'react-dom/server'
import { renderStylesToString } from '@emotion/server'
import App from './App'

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

renderStylesToNodeStream

This returns a Node Stream Writable that can be used to insert critical css right before it's required. This can be used with React's streaming API.

import { renderToNodeStream } from 'react-dom/server'
import { renderStylesToNodeStream } from '@emotion/server'
import App from './App'

const stream = renderToNodeStream(<App />).pipe(renderStylesToNodeStream())

extractCritical

This returns an object with the properties html, ids and css. It pulls out Emotion rules that are actually used in the rendered HTML, but it still includes all global rules because they don't appear in the rendered HTML as they might affect any elements on the page.

Note:

If you have dynamic global styles it's advised to create cache per single render to avoid global styles from different renders leaking into the extracted css.

import { renderToString } from 'react-dom/server'
import { extractCritical } from '@emotion/server'
import App from './App'

const { html, ids, css } = extractCritical(renderToString(<App />))

hydrate

hydrate should be called on the client with the ids that extractCritical returns. If you don't call it then Emotion will reinsert all the rules. hydrate is only required for extractCritical, not for renderStylesToString or renderStylesToNodeStream, hydration occurs automatically with renderStylesToString and renderStylesToNodeStream.

import { hydrate } from '@emotion/css'

hydrate(ids)

Next.js

To use emotion's SSR with Next.js you need a custom Document component in pages/_document.js that renders the styles and inserts them into the <head>. An example of Next.js with emotion can be found in the Next.js repo.

Note:

This only applies if you're using vanilla Emotion or a version of Emotion prior to v10. For v10 and above, SSR just works in Next.js.

Gatsby

To use emotion's SSR with Gatsby, you can use gatsby-plugin-emotion or you can do it yourself with emotion and Gatsby's various APIs but it's generally recommended to use gatsby-plugin-emotion. There's an example available in the Gatsby repo or you can look at this site which is built with Gatsby!

yarn add gatsby-plugin-emotion

gatsby-config.js

module.exports = {
  plugins: [...otherGatsbyPlugins, 'gatsby-plugin-emotion']
}

If using a custom cache, ensure you are creating a new cache per server render within gatsby-ssr.js. This will differ from the implementation within gatsby-browser.js.

create-emotion-cache.js

import createCache from '@emotion/cache'

export const createMyCache = () =>
  createCache({
    key: 'my-prefix-key',
    stylisPlugins: [
      /* your plugins here */
    ],
  })

export const myCache = createMyCache()

gatsby-ssr.js

import { CacheProvider } from '@emotion/react'

import { createMyCache } from './create-emotion-cache'

export const wrapRootElement = ({ element }) => (
  <CacheProvider value={createMyCache()}>{element}</CacheProvider>
)

gatsby-browser.js

import { CacheProvider } from '@emotion/react'

import { myCache } from './create-emotion-cache'

export const wrapRootElement = ({ element }) => (
  <CacheProvider value={myCache}>{element}</CacheProvider>
)

Note:

While Emotion 10 and above supports SSR out of the box, it's still recommended to use gatsby-plugin-emotion as gatsby-plugin-emotion will enable @emotion/babel-plugin and other potential future optimisations.

Puppeteer

If you are using Puppeteer to prerender your application, emotion's speedy option has to be disabled so that the CSS is rendered into the DOM.

index.js

// This has to be run before emotion inserts any styles so it's imported before the App component
import './disable-speedy'
import ReactDOM from 'react-dom'
import App from './App'

const root = document.getElementById('root')

// Check if the root node has any children to detect if the app has been prerendered
if (root.hasChildNodes()) {
  ReactDOM.hydrate(<App />, root)
} else {
  ReactDOM.render(<App />, root)
}

disable-speedy.js

import { sheet } from '@emotion/css'

// Check if the root node has any children to detect if the app has been preprendered
// speedy is disabled when the app is being prerendered so that styles render into the DOM
// speedy is significantly faster though so it should only be disabled during prerendering
if (!document.getElementById('root').hasChildNodes()) {
  sheet.speedy(false)
}

Note:

The sheet.speedy call has to be run before anything that inserts styles so it has to be put into it's own file that's imported before anything else.