-
Notifications
You must be signed in to change notification settings - Fork 49.4k
[Float][Fizz][Fiber] - Do not hoist elements with itemProp
& hydrate more tolerantly in hoist contexts
#26256
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
Conversation
|
||
// Creates elements in the HTML either SVG or Math namespace | ||
export function createElementNS( | ||
namespaceURI: ExoticNamespace, |
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.
responsbility is now on the caller to provide the namespace for this instance rather than the parent namespace
Comparing: e98695d...f327f7e Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: Expand to show
|
e66e7c0
to
2a4f45e
Compare
// We use this as a heuristic. It's based on intuition and not data so it | ||
// might be flawed or unnecessary. | ||
nextInstance = getNextHydratableSibling(firstAttemptedInstance); | ||
nextInstance = getNextHydratableAfterFailedAttempt(); |
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.
Something I don't get about how this used to work is
imagine in the dom there is a <!-- $ -->
but we are trying to hydrate a <div>
. The tryHydrate
would fail and if you were not in a concurrent root the getNextHydratableSibling(firstAttemptedInstance)
would return an element inside the suspense boundary.
Is there something else that would prevent this erroneous logic from occurring in the real world?
{withoutStack: 1}, | ||
); | ||
expect(Scheduler).toHaveYielded([ | ||
'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', |
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.
It's interesting that this changed. What happened before is the second suspense boundary would attempt to hydrate the <div>
from the prior step which failed b/c it was a <span>
. Hydration at this point is sort of pointless but I understand the reason is to allow these functions to warm up even though they will end up client rendering.
B/c of the changes in top level contexts (head, body, or the root of the app) the suspense boundary now skips over the
c1f8a52
to
487535a
Compare
itemProp
inside an itemScope
itemProp
} | ||
|
||
// The only branches that should fall through to here are those that need to check textContent against single value children | ||
// in particular, <style>, and <script> |
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.
Style content isn't reliable. I've found that browsers rewrite those. Not sure about script neither. It's also slow.
I don't think we need to compare this. Just assume it's the 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.
I can think of a number of ways in which a 3rd party script injecting <style>
into the body could result in the hydration binding to the wrong instance but they are probably relatively hard to hit. I'll remove this for now and we can see if there are still persistent plugins that get tripped up. We can probably move towards guiding the extension to change than React if there are not very many and their install base is small (the ones that actually get tripped up by this)
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.
Is it true that HoistingContext is only used for hydration now?
Would it be better if it was tracked in HydrationContext instead as global state just like the other hydration related stuff. That way the host config can be two different methods instead of checking a flag. It also allows the flag to be checked once when multiple steps are taken.
itemProp
itemProp
& hydrate more tolerantly in hoist contexts
Yeah I think that makes sense. I might also restore the string for HostContextProd since it might be smaller code size. I'll see if that makes a difference |
…for hoisting One recent decision was to make elements using the `itemProp` prop not hoistable if they were inside and itemScope. This better fits with Microdata spec which allows for meta tags and other tag types usually reserved for the <head> to be used in the <body> when using itemScope. To implement this a number of small changes were necessary 1. HostContext in prod needed to expand beyond just tracking the element namespace for new element creation. It now tracks whether we are in an itemScope. To keep this efficient it is modeled as a bitmask. 2. To disambiguate what is and is not a potential instance in the DOM for hoistables the hydration algo was updated to skip past non-matching instances while attempting to claim the instance rather than ahead of time (getNextHydratable). 3. React will not consider an itemScope on <html>, <head>, or <body> as a valid scope for the hoisting opt-out. This is important as an invariant so we can make assumptiosn about certain tags in these scopes. This should not be a functional breaking change because if any of these tags have an itemScope then it can just be moved into the first node inside the <body> Since we were already updating the logic for hydration to better support itemScope opt-out I also changed the hydration behavior for suspected 3rd party nodes in <head> and <body>. Now if you are hydrating in either of those contexts hydration will skip past any non-matching nodes until it finds a match. This allows 3rd party scripts and extensions to inject nodes in either context that React does not expect and still avoid a hydation mismatch. This new algorithm isn't perfect and it is possible for a mismatch to occurr. The most glarying case may be if a 3rd party script prepends a <div> into <body> and you render a <div> in <body> in your app. there is nothing to signal to React that this div was 3rd party so it will claim is as the hydrated instance and hydration will almost certainly fail immediately afterwards. The expectation is that this is rare and that if falling back to client rendering is transparent to the user then there is not problem here. We will continue to evaluate this and may change the hydration matchign algorithm further to match user and developer expectations
// hydratable but do not match the current Fiber being hydrated. We track the hydratable node we | ||
// are currently attempting using this module global. If the hydration is unsuccessful Fiber will | ||
// call getLastAttemptedHydratable which uses this cursor to return the expected next | ||
// hydratable. |
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 is quite a bug waiting to happen because it's very subtle when this needs to be reset and it'll likely not work the same when we switch to hydrating the commit phase. We need to have all state in FiberHydrationContext.
I get that you're trying to avoid returning multiple values but it might be better to just change the implementation to be closer to what the DOM actually needs.
Instead of getNextMatchingHydratableInstance, what about keeping getNextHydratableSibling for the iteration and just add something like shouldSkipHydratableInstance that's called for each one from the HydrationContext? Like that's probably more like what you would've written if you just wrote this inline without a consideration for HostConfigs.
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.
Did a refactor of the hydration context logic. Removes a bunch of branches but I'm not sure if it inlines better and has all the hydration state in FIber
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 suspect a lot of this code actually gets simpler and falls out once we remove legacy mode.
Yeah that's my feeling too |
Do not hoist elements with
itemProp
In HTML
itemprop
signifies a property of anitemscope
with respect to the Microdata spec (https://html.spec.whatwg.org/multipage/microdata.html#microdata)additionally
itemprop
is valid on any tag and can even make some tags that are otherwise invalid in the<body>
valid there (<meta>
for instance).Originally I tried an approach where if you rendered something otherwise hoistable inside an
itemscope
it would not hoist if it had anitemprop
. This meant that some components withitemprop
could hoist (if they were not scoped, which is generally invalid microdata implementation). However the problem is things that do hoist, hoist into the head and body and these tags can have anitemscope
. This creates a ton of ambiguity when trying to hydrate in these hoist scopes because we can't know for certain whether a DOM node we find there was hoisted or not even if it has anitemprop
attribute. There are other scenarios too that have abiguous semantics like rendering a hoistable withitemProp
outside of<html itemScope={true>
. Is it fair to embed that hoistable inside that itemScope even though it was defined outside?To simplify the situation and disambiguate I dropped the
itemscope
portion from the implementation and now any host component that could normally be hoisted will not hoist if it has anitemProp
prop.In addition to the changes made for
itemProp
this PR also modifies part of the hydration implementation to be more tolerant of tags injected by 3rd parties. This was opportunistically done when we needed to have context information likeinItemScope
but with the most recent implementation that has been removed. I have however left the hydration changes in place as it is a goal to make React handle hydrating the entire Document even when we cannot control whether 3rd parties are going to inject tags that React will not render but are also not hoistablesOriginal Description when we considered tracking itemScope