Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

gnoff
Copy link
Collaborator

@gnoff gnoff commented Feb 27, 2023

Do not hoist elements with itemProp

In HTML itemprop signifies a property of an itemscope 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 an itemprop. This meant that some components with itemprop 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 an itemscope. 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 an itemprop attribute. There are other scenarios too that have abiguous semantics like rendering a hoistable with itemProp 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 an itemProp 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 like inItemScope 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 hoistables


Original Description when we considered tracking itemScope

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 assumptions 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 hydration mismatch.

This new algorithm isn't perfect and it is possible for a mismatch to occur. The most glaring 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 matching algorithm further to match user and developer expectations

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Feb 27, 2023

// Creates elements in the HTML either SVG or Math namespace
export function createElementNS(
namespaceURI: ExoticNamespace,
Copy link
Collaborator Author

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

@react-sizebot
Copy link

react-sizebot commented Feb 27, 2023

Comparing: e98695d...f327f7e

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js +0.60% 155.25 kB 156.18 kB +0.90% 48.98 kB 49.42 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.59% 157.24 kB 158.17 kB +0.90% 49.65 kB 50.10 kB
facebook-www/ReactDOM-prod.classic.js +1.01% 532.50 kB 537.85 kB +0.91% 94.85 kB 95.71 kB
facebook-www/ReactDOM-prod.modern.js +1.02% 516.42 kB 521.72 kB +1.00% 92.45 kB 93.37 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
facebook-www/ReactDOMTesting-prod.modern.js +1.11% 478.42 kB 483.71 kB +0.95% 88.28 kB 89.12 kB
facebook-www/ReactDOMTesting-prod.classic.js +1.09% 494.03 kB 499.44 kB +0.99% 90.49 kB 91.39 kB
facebook-www/ReactDOM-prod.modern.js +1.02% 516.42 kB 521.72 kB +1.00% 92.45 kB 93.37 kB
facebook-www/ReactDOM-prod.classic.js +1.01% 532.50 kB 537.85 kB +0.91% 94.85 kB 95.71 kB
facebook-www/ReactDOM-profiling.modern.js +0.97% 546.66 kB 551.95 kB +0.85% 96.95 kB 97.77 kB
facebook-www/ReactDOM-profiling.classic.js +0.95% 562.83 kB 568.19 kB +0.84% 99.38 kB 100.22 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.88% 1,189.34 kB 1,199.82 kB +0.65% 264.40 kB 266.12 kB
oss-stable-semver/react-dom/cjs/react-dom.development.js +0.88% 1,187.68 kB 1,198.10 kB +0.63% 264.40 kB 266.05 kB
oss-stable/react-dom/cjs/react-dom.development.js +0.88% 1,187.70 kB 1,198.13 kB +0.63% 264.42 kB 266.08 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.87% 1,191.53 kB 1,201.96 kB +0.64% 265.18 kB 266.88 kB
oss-stable-semver/react-dom/umd/react-dom.development.js +0.87% 1,244.90 kB 1,255.75 kB +0.63% 267.27 kB 268.96 kB
oss-stable/react-dom/umd/react-dom.development.js +0.87% 1,244.93 kB 1,255.78 kB +0.63% 267.29 kB 268.98 kB
oss-experimental/react-dom/cjs/react-dom.development.js +0.87% 1,198.17 kB 1,208.60 kB +0.62% 266.42 kB 268.07 kB
oss-experimental/react-dom/umd/react-dom.development.js +0.86% 1,255.97 kB 1,266.82 kB +0.63% 269.29 kB 270.98 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.86% 1,219.74 kB 1,230.22 kB +0.61% 270.33 kB 271.98 kB
facebook-www/ReactDOM-dev.modern.js +0.79% 1,318.19 kB 1,328.66 kB +0.60% 287.53 kB 289.26 kB
facebook-www/ReactDOM-dev.classic.js +0.78% 1,345.90 kB 1,356.36 kB +0.57% 292.70 kB 294.37 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js +0.76% 858.46 kB 864.98 kB +0.29% 182.89 kB 183.41 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js +0.76% 858.48 kB 865.00 kB +0.29% 182.91 kB 183.44 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +0.75% 866.72 kB 873.24 kB +0.29% 184.41 kB 184.94 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.min.js +0.60% 108.29 kB 108.95 kB +0.69% 32.78 kB 33.01 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.min.js +0.60% 108.32 kB 108.97 kB +0.68% 32.81 kB 33.03 kB
oss-stable-semver/react-dom/umd/react-dom.production.min.js +0.60% 155.32 kB 156.25 kB +0.81% 49.57 kB 49.98 kB
oss-stable/react-dom/umd/react-dom.production.min.js +0.60% 155.34 kB 156.28 kB +0.81% 49.57 kB 49.97 kB
oss-stable-semver/react-dom/cjs/react-dom.production.min.js +0.60% 155.23 kB 156.16 kB +0.90% 48.98 kB 49.42 kB
oss-stable/react-dom/cjs/react-dom.production.min.js +0.60% 155.25 kB 156.18 kB +0.90% 48.98 kB 49.42 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.min.js +0.59% 109.82 kB 110.47 kB +0.66% 33.25 kB 33.47 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.59% 157.24 kB 158.17 kB +0.90% 49.65 kB 50.10 kB
oss-experimental/react-dom/umd/react-dom.production.min.js +0.59% 157.33 kB 158.26 kB +0.87% 50.22 kB 50.66 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.min.js +0.58% 161.63 kB 162.56 kB +0.85% 51.41 kB 51.85 kB
oss-stable-semver/react-dom/umd/react-dom.profiling.min.js +0.57% 164.29 kB 165.22 kB +0.92% 51.96 kB 52.44 kB
oss-stable/react-dom/umd/react-dom.profiling.min.js +0.57% 164.31 kB 165.24 kB +0.92% 51.96 kB 52.44 kB
oss-stable-semver/react-dom/cjs/react-dom.profiling.min.js +0.57% 164.84 kB 165.77 kB +0.81% 51.49 kB 51.91 kB
oss-stable/react-dom/cjs/react-dom.profiling.min.js +0.57% 164.86 kB 165.79 kB +0.81% 51.49 kB 51.91 kB
oss-experimental/react-dom/umd/react-dom.profiling.min.js +0.56% 166.30 kB 167.23 kB +0.81% 52.70 kB 53.13 kB
oss-experimental/react-dom/cjs/react-dom.profiling.min.js +0.56% 166.85 kB 167.79 kB +0.87% 52.17 kB 52.62 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.min.js +0.56% 117.29 kB 117.94 kB +0.76% 34.99 kB 35.26 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.min.js +0.56% 117.31 kB 117.96 kB +0.76% 35.01 kB 35.28 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.min.js +0.55% 118.81 kB 119.46 kB +0.74% 35.46 kB 35.73 kB

Generated by 🚫 dangerJS against f327f7e

// 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();
Copy link
Collaborator Author

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.',
Copy link
Collaborator Author

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

(assuming it was inserted by some 3rd party) and finds the expected Suspense boundary that follows. This no longer leads to a second hydration error

@gnoff gnoff force-pushed the float-itemprop branch 2 times, most recently from c1f8a52 to 487535a Compare March 4, 2023 00:45
@gnoff gnoff changed the title [Float][Fizz][Fiber] - Do not hoist elements with itemProp inside an itemScope [Float][Fizz][Fiber] - Do not hoist elements with itemProp Mar 4, 2023
}

// 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>
Copy link
Collaborator

@sebmarkbage sebmarkbage Mar 4, 2023

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.

Copy link
Collaborator Author

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)

Copy link
Collaborator

@sebmarkbage sebmarkbage left a 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.

@gnoff gnoff changed the title [Float][Fizz][Fiber] - Do not hoist elements with itemProp [Float][Fizz][Fiber] - Do not hoist elements with itemProp & hydrate more tolerantly in hoist contexts Mar 4, 2023
@gnoff
Copy link
Collaborator Author

gnoff commented Mar 4, 2023

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

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

gnoff added 2 commits March 4, 2023 11:04
…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.
Copy link
Collaborator

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.

Copy link
Collaborator Author

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

Copy link
Collaborator

@sebmarkbage sebmarkbage left a 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.

@gnoff
Copy link
Collaborator Author

gnoff commented Mar 6, 2023

I suspect a lot of this code actually gets simpler and falls out once we remove legacy mode.

Yeah that's my feeling too

@gnoff gnoff merged commit 8a9f82e into facebook:main Mar 6, 2023
@gnoff gnoff deleted the float-itemprop branch March 6, 2023 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants