Skip to content

Commit

Permalink
Fix moving of client-side inserted style tags from Emotion 10 when in…
Browse files Browse the repository at this point in the history
…tending to hydrate Emotion 11 styles resulting in losing styles (#2361)

* Fixes cache provider touching style nodes owned by a seperate key

* Only mount body styles to the head

* Fix things

* Address PR feedback

* Update test

* Create orange-buttons-bow.md

* Attribute the change to both Daniel and Mitchell

Co-authored-by: mitchellhamilton <[email protected]>
Co-authored-by: Mateusz Burzyński <[email protected]>
  • Loading branch information
3 people authored May 5, 2021
1 parent 780bc17 commit 38f9d44
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 5 deletions.
8 changes: 8 additions & 0 deletions .changeset/orange-buttons-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@emotion/cache': patch
---

author: @danieldelcore
author: @mitchellhamilton

Fixed moving of client-side inserted style tags from Emotion 10 when intending to hydrate Emotion 11 styles resulting in losing styles in production
146 changes: 146 additions & 0 deletions packages/cache/__tests__/hydration.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,149 @@ test('rehydrated styles to head can be flushed', () => {
cache.sheet.flush()
expect(document.documentElement).toMatchSnapshot()
})

test('flushing rehydrated styles in the head only affect styles matching the cache key', () => {
safeQuerySelector('head').innerHTML = [
'<style data-emotion="emo 1lrxbo5">.emo-1lrxbo5{color:hotpink;}</style>',
'<style data-emotion="css qweqwee">.css-qweqwee{color:red;}</style>'
].join('')

// this moves emotion style tags at initialization time
jest.resetModules()
require('@emotion/react')

const cache = createCache({ key: 'emo' })
expect(document.documentElement).toMatchInlineSnapshot(`
<html>
<head>
<style
data-emotion="css qweqwee"
data-s=""
>
.css-qweqwee{color:red;}
</style>
<style
data-emotion="emo 1lrxbo5"
data-s=""
>
.emo-1lrxbo5{color:hotpink;}
</style>
</head>
<body />
</html>
`)

cache.sheet.flush()
expect(document.documentElement).toMatchInlineSnapshot(`
<html>
<head>
<style
data-emotion="css qweqwee"
data-s=""
>
.css-qweqwee{color:red;}
</style>
</head>
<body />
</html>
`)
})

test('should only hydrate style elements matching the cache key', () => {
let css = `color:hotpink;`
let hash = hashString(css)

safeQuerySelector(
'body'
).innerHTML = `<style data-emotion="emo ${hash}">.emo-${hash}{${css}}</style>`

const cache = createCache({ key: 'custom-key' })

expect(cache.inserted).toEqual({})
expect(document.documentElement).toMatchInlineSnapshot(`
<html>
<head />
<body>
<style
data-emotion="emo 1lrxbo5"
>
.emo-1lrxbo5{color:hotpink;}
</style>
</body>
</html>
`)

const cache2 = createCache({ key: 'emo' })

expect(cache2.inserted).toEqual({ [hash]: true })
expect(document.documentElement).toMatchInlineSnapshot(`
<html>
<head>
<style
data-emotion="emo 1lrxbo5"
>
.emo-1lrxbo5{color:hotpink;}
</style>
</head>
<body />
</html>
`)
})

test('Existing client-side inserted styles from Emotion 10 should not be moved', () => {
// the nested nature isn't special, it's just meant to be a general "make sure they're not moved"
safeQuerySelector(
'body'
).innerHTML = `<div><style data-emotion="css-global"></style><div><style data-emotion="css"></style></div></div>`
expect(document.documentElement).toMatchInlineSnapshot(`
<html>
<head />
<body>
<div>
<style
data-emotion="css-global"
/>
<div>
<style
data-emotion="css"
/>
</div>
</div>
</body>
</html>
`)

const css = `color:hotpink;`
const hash = hashString(css)
let thing = document.createElement('div')
thing.innerHTML = `<style data-emotion="css ${hash}">.css-${hash}{${css}}</style>`
safeQuerySelector('body').appendChild(thing)
jest.resetModules()
require('@emotion/react')

expect(document.documentElement).toMatchInlineSnapshot(`
<html>
<head>
<style
data-emotion="css 1lrxbo5"
data-s=""
>
.css-1lrxbo5{color:hotpink;}
</style>
</head>
<body>
<div>
<style
data-emotion="css-global"
/>
<div>
<style
data-emotion="css"
/>
</div>
</div>
<div />
</body>
</html>
`)
})
25 changes: 20 additions & 5 deletions packages/cache/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,25 @@ let createCache = (options: Options): EmotionCache => {
const ssrStyles = document.querySelectorAll(
`style[data-emotion]:not([data-s])`
)

// get SSRed styles out of the way of React's hydration
// document.head is a safe place to move them to
// document.head is a safe place to move them to(though note document.head is not necessarily the last place they will be)
// note this very very intentionally targets all style elements regardless of the key to ensure
// that creating a cache works inside of render of a React component
Array.prototype.forEach.call(ssrStyles, (node: HTMLStyleElement) => {
// we want to only move elements which have a space in the data-emotion attribute value
// because that indicates that it is an Emotion 11 server-side rendered style elements
// while we will already ignore Emotion 11 client-side inserted styles because of the :not([data-s]) part in the selector
// Emotion 10 client-side inserted styles did not have data-s (but importantly did not have a space in their data-emotion attributes)
// so checking for the space ensures that loading Emotion 11 after Emotion 10 has inserted some styles
// will not result in the Emotion 10 styles being destroyed
const dataEmotionAttribute = ((node.getAttribute(
'data-emotion'
): any): string)
if (dataEmotionAttribute.indexOf(' ') === -1) {
return
}

;((document.head: any): HTMLHeadElement).appendChild(node)
node.setAttribute('data-s', '')
})
Expand All @@ -82,14 +98,13 @@ let createCache = (options: Options): EmotionCache => {
container = options.container || ((document.head: any): HTMLHeadElement)

Array.prototype.forEach.call(
document.querySelectorAll(`style[data-emotion]`),
// this means we will ignore elements which don't have a space in them which
// means that the style elements we're looking at are only Emotion 11 server-rendered style elements
document.querySelectorAll(`style[data-emotion^="${key} "]`),
(node: HTMLStyleElement) => {
const attrib = ((node.getAttribute(`data-emotion`): any): string).split(
' '
)
if (attrib[0] !== key) {
return
}
// $FlowFixMe
for (let i = 1; i < attrib.length; i++) {
inserted[attrib[i]] = true
Expand Down

0 comments on commit 38f9d44

Please sign in to comment.