fix: render state not unique per render#1638
Merged
marvinhagemeister merged 13 commits intoAug 16, 2023
Merged
Conversation
This is done so that there is less risk of accidentally introducing a race condition due to global variables
31a84ce to
3222793
Compare
bartlomieju
reviewed
Aug 16, 2023
bartlomieju
left a comment
Contributor
There was a problem hiding this comment.
LGTM, have you measure the performance impact of this change?
| // Keep track of which component rendered which vnode. This allows us | ||
| // to detect when an island is rendered within another instead of being | ||
| // passed as children. | ||
| let ownerStack: VNode[] = []; |
Contributor
There was a problem hiding this comment.
This appears to still be global - won't this cause any problems?
Contributor
Author
There was a problem hiding this comment.
We cannot completely remove all globals unless we add some sort of render context API to Preact. This one here in particular is always replaced when a new rendered is triggered here https://github.com/denoland/fresh/pull/1638/files#diff-85a0f65cd2b2187632bf4c5c3c0385eae00d0d0cd49feac175ca59558b02490fR50
bartlomieju
approved these changes
Aug 16, 2023
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR addresses a rendering correctness issue. In #1636 it was discovered that serialized island props were shared across renders when the server dealt with multiple requests at the same time. I did a bisect locally and the issue seems to be present since at least Fresh 1.0.
The problem
The gist of the problem is that we need to set up some global variables to track what Preact is rendering and to be aware of when we are trying to render an island so that we can inject markers in the HTML for the browser to continue from. This is fine in synchronous code and rendering with
preact-render-to-stringworks that way anyway. Async route component don't change that, because they are not treated as a component, but rather a normal function that happens to return JSX.Completely oversimplified the situation looked like this:
The problem is the async bit in between. There is no guarantee that you're dealing with the same global variables after the Promise is awaited. The globals are effectively leaked across renders. There were some problematic usage of globals similar to this scenario too.
The solution
The code has been rewritten so that you have to pass a rendering context around and cannot use globals. Instead of resetting the remaining globals at the start of the function and risk introducing asynchronicity later, that part has moved to be as close as possible to the
renderToStringcall.Whilst the basic problem is simple to understand, the rendering code in Fresh relied on a lot of assumptions and used Preact's
options.vnode-API incorrectly. It assumed that it would be called as part of the synchronous render pass, which there is no guarantee for. The only guarantee this hook gives is that it's called when a vnode is created. But vnodes can be created anywhere, not just during rendering. A simple example where a vnode is created outside of rendering:Longterm, the solution is to upstream the render state context API to
preact-render-to-string, so that these kind of API mis-usages cannot happen and that no manual globals are needed.In essence this PR therefore rewrites our whole rendering pipeline to get rid of any potential places to leak state across renders. This unfortunately makes the PR pretty big, but the new rendering code is much easier to follow now as a plus.
Fixes #1636