@samitouri / QOS-React / commits / 17204056d5

[Float] fix coordination of resource identity and hydration (#25569)

there are a few bugs where dom representations from SSR aren't identified as Resources when they should be. There are 3 semantics Resource -> hoist to head, deduping, etc... hydratable Component -> SSR'd and hydrated in place non-hydratable Component -> never SSR'd, never hydrated, always inserted on the client this last category is small (non stylesheet) links with onLoad and/or onError async scripts with onLoad and/or onError The reason we have this distinction for now is we need every SSR'd async script to be assumable to be a Resource. we don't currently encode onLoad on the server and so we couldn't otherwise tell if an async script is a Resource or is an async script with an onLoad which would not be a resource. To avoid this ambiguity we never emit the scripts in SSR and assume they need to be inserted on the client. We can explore changes to these semantics in the future or possibly encode some identifier when we want to opt out of resource semantics but still SSR the link or script.

Josh Story committed Oct 26, 2022 at 19:53 UTC 17204056d5ec0a53a74979321c519137d215cf64
4 files changed +94 -22
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+2 -2
@@ -1465,9 +1465,9 @@ export function isHostResourceType(type: string, props: Props): boolean {
1465 }
1466 return (async: any) && typeof src === 'string' && !onLoad && !onError;
1467 }
1468 + case 'noscript':
1469 case 'template':
1469 - case 'style':
1470 - case 'noscript': {
1470 + case 'style': {
1471 if (__DEV__) {
1472 if (resourceFormOnly) {
1473 console.error(
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+33 -19
@@ -802,7 +802,15 @@ export const supportsHydration = true;
802 // inserted without breaking hydration
803 export function isHydratable(type: string, props: Props): boolean {
804 if (enableFloat) {
805 - if (type === 'script') {
805 + if (type === 'link') {
806 + if (
807 + (props: any).rel === 'stylesheet' &&
808 + typeof (props: any).precedence !== 'string'
809 + ) {
810 + return true;
811 + }
812 + return false;
813 + } else if (type === 'script') {
814 const {async, onLoad, onError} = (props: any);
815 return !(async && (onLoad || onError));
816 }
@@ -902,16 +910,25 @@ function getNextHydratable(node) {
910 if (nodeType === ELEMENT_NODE) {
911 const element: Element = (node: any);
912 switch (element.tagName) {
913 + case 'TITLE':
914 + case 'META':
915 + case 'BASE':
916 + case 'HTML':
917 + case 'HEAD':
918 + case 'BODY': {
919 + continue;
920 + }
921 case 'LINK': {
922 const linkEl: HTMLLinkElement = (element: any);
907 - const rel = linkEl.rel;
923 + // All links that are server rendered are resources except
924 + // stylesheets that do not have a precedence
925 if (
909 - rel === 'preload' ||
910 - (rel === 'stylesheet' && linkEl.hasAttribute('data-precedence'))
926 + linkEl.rel === 'stylesheet' &&
927 + !linkEl.hasAttribute('data-precedence')
928 ) {
912 - continue;
929 + break;
930 }
914 - break;
931 + continue;
932 }
933 case 'STYLE': {
934 const styleEl: HTMLStyleElement = (element: any);
@@ -927,12 +944,6 @@ function getNextHydratable(node) {
944 }
945 break;
946 }
930 - case 'TITLE':
931 - case 'HTML':
932 - case 'HEAD':
933 - case 'BODY': {
934 - continue;
935 - }
947 }
948 break;
949 } else if (nodeType === TEXT_NODE) {
@@ -942,18 +953,21 @@ function getNextHydratable(node) {
953 if (nodeType === ELEMENT_NODE) {
954 const element: Element = (node: any);
955 switch (element.tagName) {
956 + case 'TITLE':
957 + case 'META':
958 + case 'BASE': {
959 + continue;
960 + }
961 case 'LINK': {
962 const linkEl: HTMLLinkElement = (element: any);
947 - const rel = linkEl.rel;
963 + // All links that are server rendered are resources except
964 + // stylesheets that do not have a precedence
965 if (
949 - rel === 'preload' ||
950 - (rel === 'stylesheet' && linkEl.hasAttribute('data-precedence'))
966 + linkEl.rel === 'stylesheet' &&
967 + !linkEl.hasAttribute('data-precedence')
968 ) {
952 - continue;
969 + break;
970 }
954 - break;
955 - }
956 - case 'TITLE': {
971 continue;
972 }
973 case 'STYLE': {
packages/react-dom-bindings/src/server/ReactDOMFloatServer.js
+1 -1
@@ -863,7 +863,7 @@ export function resourcesFromLink(props: Props): boolean {
863 }
864 }
865 if (props.onLoad || props.onError) {
866 - return false;
866 + return true;
867 }
868
869 const sizes = typeof props.sizes === 'string' ? props.sizes : '';
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+58
@@ -281,6 +281,64 @@ describe('ReactDOMFloat', () => {
281 });
282 }
283
284 + // @gate enableFloat
285 + it('can hydrate non Resources in head when Resources are also inserted there', async () => {
286 + await actIntoEmptyDocument(() => {
287 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
288 + <html>
289 + <head>
290 + <meta property="foo" content="bar" />
291 + <link rel="foo" href="bar" onLoad={() => {}} />
292 + <title>foo</title>
293 + <base target="foo" href="bar" />
294 + <script async={true} src="foo" onLoad={() => {}} />
295 + </head>
296 + <body>foo</body>
297 + </html>,
298 + );
299 + pipe(writable);
300 + });
301 + expect(getMeaningfulChildren(document)).toEqual(
302 + <html>
303 + <head>
304 + <base target="foo" href="bar" />
305 + <link rel="preload" href="foo" as="script" />
306 + <meta property="foo" content="bar" />
307 + <title>foo</title>
308 + </head>
309 + <body>foo</body>
310 + </html>,
311 + );
312 +
313 + ReactDOMClient.hydrateRoot(
314 + document,
315 + <html>
316 + <head>
317 + <meta property="foo" content="bar" />
318 + <link rel="foo" href="bar" onLoad={() => {}} />
319 + <title>foo</title>
320 + <base target="foo" href="bar" />
321 + <script async={true} src="foo" onLoad={() => {}} />
322 + </head>
323 + <body>foo</body>
324 + </html>,
325 + );
326 + expect(Scheduler).toFlushWithoutYielding();
327 + expect(getMeaningfulChildren(document)).toEqual(
328 + <html>
329 + <head>
330 + <base target="foo" href="bar" />
331 + <link rel="preload" href="foo" as="script" />
332 + <meta property="foo" content="bar" />
333 + <title>foo</title>
334 + <link rel="foo" href="bar" />
335 + <script async="" src="foo" />
336 + </head>
337 + <body>foo</body>
338 + </html>,
339 + );
340 + });
341 +
342 // @gate enableFloat || !__DEV__
343 it('warns if you render resource-like elements above <head> or <body>', async () => {
344 const root = ReactDOMClient.createRoot(document);