-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed issue with cache created in render crashing SSRed site #1572
Conversation
🦋 Changeset is good to goLatest commit: b9b9a44 We got this. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
packages/cache/src/index.js
Outdated
if (isBrowser) { | ||
ssrStyles = document.querySelectorAll(`style[data-emotion]`) | ||
Array.prototype.forEach.call(ssrStyles, (node: HTMLStyleElement) => { | ||
document.head.appendChild(node) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need to move those before render
, head seems like a "safe place"
packages/cache/src/index.js
Outdated
// $FlowFixMe | ||
attrib.split(' ').forEach(id => { | ||
inserted[id] = true | ||
}) | ||
if (node.parentNode !== container) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we might need to reparent here what already got moved previously to head if the container is a custom one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds look a good approach to me
Ok, I'll try later adding backward compatibility and some tests. Thanks for the feedback. |
I think we might want to wait for v11 to do this because otherwise we have to decide between elements having different attributes which would be bad for users expecting them to have the attributes they have right now or writing both attributes which would mean larger SSR payloads. |
Yeah, you are probably right. I've created v11 milestone and added this and a bunch of other stuff to it. The list is certainly not complete yet, I need to go through all issues first before I call it complete - but if you have anything you'd like to add there, please feel free to do so 😉 |
I am not sure this is a good idea, it makes emotion modules not pure because they effect things outside of themselves. This will affect tree shaking etc. I don't know how the automatic hydration works now though, so this may already be an issue. Also wouldn't the example cause issues due to re-creating the cache on each render. If you wanted to support this scenario in addition to below you need to memoize createCache based on the key. I have a few ideas on how this should be resolved 1. Introduce a factory to create MyWrapperThis doesn't affect emotion at all. const createMyWrapper = key => {
const myCache = createCache({ key })
const MyWrapper = ({ children }) => {
return <CacheProvider value={myCache}>{children}</CacheProvider>
}
return MyWrapper
}
const MyWrapper = createMyWrapper('stl')
export default () => (
<MyWrapper>
<Component />
</MyWrapper>
)
function Component() {
return <div css={css`
color: green;
`}>
Hello world!
</div>;
} 2. Update the hydrate function in create-emotion to do the hoisting to headThis also requires you rendering the Best place for this seems to be inside jsx.js, you could render an additional style element then mark it as inserted on the cache? Then hydrate could look something like this:
|
Example code for option 2
|
This definitely is a downside of this.
Not that much, the side effect is short and concise - but yeah, in general this eager call cannot be tree shaken.
Automatic hydration happens at the moment inside of the emotion/packages/cache/src/index.js Lines 68 to 83 in ed0a460
For this to work automatically each request has to have its own unique cache because we need to track which exact rules are needed for the current SSR output and we want to avoid duplication when the same rule gets used multiple times. This cache gets created~ automatically here (it's created by BasicProvider): emotion/packages/core/src/context.js Lines 58 to 72 in ed0a460
So as described above - this already happens (on the server-side with automatic SSR). |
Understand on the server, it's only a single pass. But what about on the client? If the user is calling createCache inside a render method, I can't see any caching of the return value anywhere? |
The client is using different strategy - it uses plain default cache from context's default value and doesn't recreate it on each render (when not customizing cache etc). |
Yeah, but the issue was creating a custom cache with a different key during render? |
A consumer using a custom cache on the client should cache it themselves with |
Not sure where we are at. But IMO I don't think this change is needed. The pattern used in the original issue is broken for a few reasons. I think you should just add some docs that caches should not be created during rendering if using SSR. There are heaps of ways to solve the issue once you put that constraint on. |
Why is the pattern in the original issue broken besides the fact that the creation of the cache wasn’t memoized? It’s also important to call out that we need to move some style tags on initial load always anyway because of the default cache so this doesn’t really cause any more code to run |
Memoizing the cache creation still causes the original issue because style tags are being removed during a render. The default cache works because this is done when you load the library, not during initial hydration. I don't mind this change, as long as it doesn't impact extract critical |
This PR moves all the tags to the head before render so that’s not an issue since at most, the styles tags will be moved from the head to somewhere else and importantly they won’t be in the tree managed by React so it won’t cause any problems.(though in most cases they’ll stay in the head) |
The docs would just need to include memoizing the cache (or building this in), and instructions on how to hydrate if that isn't done automatically. |
I think we all here understand the core issue (based on recent comments) - but some older comments look a little bit confused, so I've included a little bit more thorough explanation in the original post. After some thinking about this, I'm more convinced now that we shouldn't merge this. This requires some side-effectful code being executed in a top-level scope during initialization and I would consider this to be an anti-pattern. This can never be eliminated by minification tools - which is not that big of a deal but indicates a code smell. The whole thing of moving style tags around is already a hack (a clever one) and I don't think we need to make it even hackier. Its purpose is to provide a 0 config SSR, but when one creates a custom cache it already is past the "0 config" point. It would, of course, be better if things would just work under all possible circumstances - but in here we really have received a single report about this in 1 year in which emotion@10 is out, so it seems like documentation around this can be enough. We could try to think how to warn people at runtime about this better - one thing that comes to my mind that we could track in dev jsx calls (so basically rendering) and Other than that we could also propose a solution for this as it seems like a use case itself is valid. It might in some edge case scenarios be easier to create cache during render (where one has access to props etc) rather than per request (although it's always possible to create it per request). A solution would be to just move those style tags "manually" to head or any other destination container of choosing (we could make it easier by exposing a helper for this - which we would use ourselves internally as well) |
We already have to move style elements for the default cache to the head though so this only means that we have to move some more style elements so I don't really see the problem? |
What we do currently is a little bit hacky (although clever) - but it's at least contained within a call that returns something. This has potential (mostly theoretical) of being removed by DCE. What I don't like the most after sitting on this PR for a bit that it's guesswork - we try to move everything into the head and later we try to correct ourselves when this is not what was supposed to happen. The current solution is at least always targeted from the start. |
@mitchellhamilton any more thoughts on this one? How do we want to proceed? |
I'm still of the opinion that we should do this.
I don't think this really matters since we will always create a default cache so we're always going to be running this code
While, yes, it will sometimes be wrong, I think it's good enough because in most cases the container option is used for iframes rather than a custom container in the same page and given that the SSRed style tags aren't in the container before they're moved, I don't think it's a big deal. |
The problem I see is about transitive dependencies. Imagine a situation like this - a component library built without emotion but with its select component built on top of Maybe we could wrap this side effect inside a |
@mitchellhamilton I've roughly adjusted the code. Please review with extra care as this might be considered a major breaking change - it changes all |
packages/core/src/context.js
Outdated
// document.head is a safe place to move them to | ||
Array.prototype.forEach.call(ssrStyles, (node: HTMLStyleElement) => { | ||
;((document.head: any): HTMLHeadElement).appendChild(node) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should happen inside @emotion/cache
since otherwise this will be broken for emotion
/to be renamed to @emotion/css
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How could it affect vanilla emotion
? This thing here only fixes moving "inline" style tags (produced by zero-config SSR) - vanilla emotion
is not affected by this as it doesn't produce such style tags, so it should be safe to have this in @emotion/core
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use renderStylesToString
and renderStylesToNodeStream
with vanilla emotion which both produce inline style tags
> | ||
|
||
h1{-webkit-animation:animation-ocj8pk 1s;animation:animation-ocj8pk 1s;} | ||
@keyframes animation-ocj8pk{from,to{color:green;}50%{color:hotpink;}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reordering of things worries me, could we revert the global refactoring since from what I understand, it's solving a bug or anything, only size/code niceness, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change could have fixed a slight bug with sheet.before
- but I need to give it slightly more thought. Probably better to move this out of this PR so I've reverted this change for now.
e41f16f
to
746e37a
Compare
Codecov Report
|
After some more thinking I have one more concern about this (although it's not strictly exclusive to this change, it relates to it). Because of one of the latest changes - https://github.com/emotion-js/emotion/pull/1696/files#diff-af18f1f9e18306ed3fc3d28811507a87R5 - cache/sheet owns its SSRed styles, which is a good thing because they are just another styles, only inserted to the document in the other point in time (SSR). Given this fact, we shouldn't allow for a situation where a single style tag is owned by multiple sheets/caches and right now it happens by default - because the same I see 2 options how we could handle this:
|
I'm sure I really understand the problem with style tags being "owned" by multiple sheets/what problems it would cause? |
Flushing is the problem. One cache might call flush and it might remove styles inserted by a different one. EDIT:// oh - and even worse problem than flushing. Different caches can have different containers, we cant allow for a newly created cache to “hijack” existing styles into its custom container |
I think that those problems are acceptable since flushing is a pretty rare thing and hijacking styles into a custom container shouldn't be a problem since you'll only use a cache with a custom container in one of two scenarios:
|
Idea: what if we made key a required option on the cache |
Yes, this is pretty much my thinking - with the addition of introducing dev-only warnings about duplicate keys (maybe just for browser envs as on the server caches are recreated each time). That would also kinda imply that maybe we should use different keys for |
dc2ff9d
to
3199270
Compare
3199270
to
7966846
Compare
@mitchellhamilton Ok, I've rebased and reimplemented this slightly. Would appreciate a re-review. |
@mitchellhamilton good catch with that options argument! anything else we'd like to change here? or is this ready to merge? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs a changeset but other than that, LGTM
I've added changesets & I'm merging this in, but please take a look at them. |
…-js#1572) * Fixed issue with cache created in render crashing SSRed site * Update index.d.ts * Update packages/cache/src/index.js * Fix flow errors * add changesets Co-authored-by: Mitchell Hamilton <mitchell@hamil.town>
fixes #1468
This is currently just a proof-of-concept. I'd like to hear out the review before proceeding with this further.
Code like this:
is crashing SSRed site at runtime because React gets confused a lot by this as most likely it had already chance to recognize style tags as being rendered, but we move those styles inside of render (so during rehydration).
This is not a backward-compatible solution yet, because I had to change the rendered data attribute and the change affects more than one package so it's not self-contained. Thoughts on that? We can try to handle both cases at runtime (old and new one)
A little bit more detailed explanation
The issue was caused by server/client mismatch - React has started rendering (and thus seen the current state of the DOM with style tags rendered besides regular components). Moving style tags to a final container (document.head by default) happen inside
createCache
, so it was too late in the case of a cache being created in the render. At least that was what I've concluded from all of this.I've tried to recreate repro case on CodeSandbox but I have no luck - all I can get is a mismatch error: https://codesandbox.io/s/jolly-jang-idu3r . I've also tried using production react-dom there but it didn't change anything.
I'm not entirely sure how React treats mismatched content but it seems like it tries to patch things up in this scenario - and as it has seen style as part of the rendered output (and that it's actually first child) it tries to:
createCache
and the style tag has changed a parent (surprisingly for React)