Skip to content
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

Add useOpaqueIdentifier Hook #17322

Merged
merged 7 commits into from
Apr 7, 2020
Merged

Add useOpaqueIdentifier Hook #17322

merged 7 commits into from
Apr 7, 2020

Conversation

lunaruan
Copy link
Contributor

@lunaruan lunaruan commented Nov 8, 2019

We currently use unique IDs in a lot of places. Examples are:
* <label for="ID">
* aria-labelledby

This can cause some issues:
1. If we server side render and then hydrate, this could cause an hydration ID mismatch
2. If we server side render one part of the page and client side render another part of the page, the ID for one part could be different than the ID for another part even though they are supposed to be the same
3. If we conditionally render something with an ID , this might also cause an ID mismatch because the ID will be different on other parts of the page

This PR creates a new hook useOpaqueIdentifier that generates a different unique ID based on whether the hook was called on the server or client. If the hook is called during hydration, it generates an opaque object that will rerender the hook so that the IDs match.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Nov 8, 2019

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 815042f:

Sandbox Source
suspicious-wildflower-jwrgu Configuration

@sizebot
Copy link

sizebot commented Nov 8, 2019

Details of bundled changes.

Comparing: 7785a52...815042f

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-server.browser.development.js +14.3% +13.8% 135.47 KB 154.78 KB 34.7 KB 39.47 KB UMD_DEV
ReactTestUtils-dev.js +0.1% +0.2% 59.47 KB 59.56 KB 16.03 KB 16.06 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 🔺+16.5% 🔺+15.6% 20.01 KB 23.32 KB 7.41 KB 8.57 KB UMD_PROD
react-dom.profiling.min.js +0.4% +0.4% 126.99 KB 127.54 KB 39.63 KB 39.78 KB NODE_PROFILING
react-dom-unstable-fizz.browser.development.js +1.9% +1.5% 4.82 KB 4.91 KB 1.62 KB 1.64 KB UMD_DEV
ReactDOM-dev.js +0.5% +0.5% 966.36 KB 971.45 KB 214.23 KB 215.37 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.4% 🔺+0.4% 405.67 KB 407.11 KB 73.2 KB 73.51 KB FB_WWW_PROD
ReactDOM-profiling.js +0.3% +0.5% 416.49 KB 417.93 KB 75.03 KB 75.37 KB FB_WWW_PROFILING
react-dom-unstable-fizz.browser.development.js +2.0% +1.6% 4.35 KB 4.43 KB 1.52 KB 1.55 KB NODE_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1.02 KB 1.02 KB 619 B 618 B NODE_PROD
react-dom-test-utils.development.js +0.1% +0.2% 61.82 KB 61.91 KB 16.38 KB 16.41 KB UMD_DEV
ReactDOMTesting-dev.js +0.6% +0.5% 920.76 KB 925.86 KB 205.02 KB 206.14 KB FB_WWW_DEV
ReactDOMTesting-prod.js 🔺+0.4% 🔺+0.5% 391.14 KB 392.58 KB 71.11 KB 71.43 KB FB_WWW_PROD
react-dom-test-utils.development.js +0.2% +0.2% 57.19 KB 57.28 KB 15.74 KB 15.77 KB NODE_DEV
ReactDOMTesting-profiling.js +0.4% +0.5% 391.14 KB 392.58 KB 71.11 KB 71.43 KB FB_WWW_PROFILING
react-dom-server.node.development.js +13.2% +12.9% 129.73 KB 146.88 KB 34.53 KB 38.98 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 12.23 KB 12.23 KB 4.5 KB 4.51 KB NODE_PROD
react-dom-server.node.production.min.js 🔺+16.1% 🔺+15.6% 20.33 KB 23.62 KB 7.53 KB 8.7 KB NODE_PROD
react-dom-server.browser.development.js +13.3% +13.0% 128.51 KB 145.66 KB 34.27 KB 38.72 KB NODE_DEV
react-dom-server.browser.production.min.js 🔺+16.5% 🔺+15.8% 19.92 KB 23.21 KB 7.38 KB 8.54 KB NODE_PROD
react-dom.development.js +0.6% +0.6% 925.67 KB 930.89 KB 201.94 KB 203.13 KB UMD_DEV
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 56.11 KB 56.11 KB 13.85 KB 13.85 KB UMD_DEV
react-dom.production.min.js 🔺+0.4% 🔺+0.6% 122.84 KB 123.38 KB 39.14 KB 39.36 KB UMD_PROD
ReactDOMForked-dev.js +0.5% +0.5% 966.36 KB 971.45 KB 214.23 KB 215.37 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 10 KB 10 KB 3.37 KB 3.36 KB UMD_PROD
ReactDOMServer-dev.js +13.5% +13.6% 135.92 KB 154.25 KB 34.73 KB 39.45 KB FB_WWW_DEV
react-dom.profiling.min.js +0.4% +0.5% 126.66 KB 127.2 KB 40.4 KB 40.61 KB UMD_PROFILING
ReactDOMForked-prod.js 🔺+0.4% 🔺+0.4% 405.67 KB 407.11 KB 73.2 KB 73.51 KB FB_WWW_PROD
ReactDOMServer-prod.js 🔺+13.6% 🔺+17.8% 45.87 KB 52.11 KB 10.69 KB 12.59 KB FB_WWW_PROD
react-dom.development.js +0.6% +0.6% 881.44 KB 886.37 KB 199.48 KB 200.66 KB NODE_DEV
ReactDOMForked-profiling.js +0.3% +0.5% 416.49 KB 417.93 KB 75.03 KB 75.37 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 53.21 KB 53.21 KB 13.65 KB 13.65 KB NODE_DEV
react-dom-unstable-fizz.node.development.js +1.7% +1.4% 5.09 KB 5.18 KB 1.68 KB 1.7 KB NODE_DEV
react-dom.production.min.js 🔺+0.4% 🔺+0.4% 123.01 KB 123.56 KB 38.4 KB 38.57 KB NODE_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 9.75 KB 9.75 KB 3.26 KB 3.26 KB NODE_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.5% +0.5% 562.52 KB 565.56 KB 117.07 KB 117.62 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.3% 🔺+0.2% 73.76 KB 73.95 KB 22.43 KB 22.48 KB UMD_PROD
react-test-renderer-shallow.development.js +0.2% +0.3% 38.63 KB 38.72 KB 9.4 KB 9.42 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.0% 12.74 KB 12.74 KB 3.97 KB 3.97 KB UMD_PROD
react-test-renderer.development.js +0.5% +0.5% 536.36 KB 539.21 KB 115.63 KB 116.18 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.3% 🔺+0.3% 73.59 KB 73.78 KB 22.12 KB 22.19 KB NODE_PROD
ReactTestRenderer-dev.js +0.5% +0.5% 570.26 KB 573.17 KB 120.29 KB 120.86 KB FB_WWW_DEV

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +0.5% +0.4% 580.98 KB 583.71 KB 121.97 KB 122.47 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.1% 🔺+0.1% 240.67 KB 240.99 KB 40.67 KB 40.72 KB FB_WWW_PROD
react-art.development.js +0.4% +0.4% 645 KB 647.83 KB 135.52 KB 136 KB UMD_DEV
react-art.production.min.js 🔺+0.2% 🔺+0.1% 109.32 KB 109.51 KB 33.02 KB 33.07 KB UMD_PROD
react-art.development.js +0.5% +0.4% 548.97 KB 551.64 KB 117.91 KB 118.4 KB NODE_DEV
react-art.production.min.js 🔺+0.2% 🔺+0.2% 74.31 KB 74.49 KB 22.22 KB 22.27 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.4% +0.3% 650.84 KB 653.57 KB 139.63 KB 140.11 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.1% 🔺+0.1% 270.59 KB 270.91 KB 46.57 KB 46.6 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.1% +0.1% 282.49 KB 282.81 KB 48.77 KB 48.8 KB RN_FB_PROFILING
ReactFabric-dev.js +0.4% +0.3% 630.14 KB 632.87 KB 134.87 KB 135.34 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.1% 🔺+0.1% 262.43 KB 262.75 KB 45.03 KB 45.06 KB RN_OSS_PROD
ReactFabric-profiling.js +0.1% +0.1% 274.33 KB 274.65 KB 47.25 KB 47.28 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.4% +0.4% 632.56 KB 635.28 KB 135.18 KB 135.65 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.1% 🔺+0.1% 262.58 KB 262.9 KB 45.06 KB 45.1 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.4% +0.3% 648.43 KB 651.16 KB 139.3 KB 139.78 KB RN_OSS_DEV
ReactFabric-profiling.js +0.1% +0.1% 274.48 KB 274.8 KB 47.28 KB 47.31 KB RN_FB_PROFILING
ReactNativeRenderer-prod.js 🔺+0.1% 🔺+0.1% 270.44 KB 270.76 KB 46.53 KB 46.57 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.1% +0.1% 282.35 KB 282.67 KB 48.74 KB 48.77 KB RN_OSS_PROFILING

react-debug-tools

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-debug-tools.development.js +12.1% +11.8% 18.84 KB 21.12 KB 5.16 KB 5.77 KB NODE_DEV
react-debug-tools.production.min.js 🔺+5.3% 🔺+5.2% 5.65 KB 5.95 KB 2.16 KB 2.27 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-reflection.development.js +0.5% +0.6% 16.13 KB 16.22 KB 4.88 KB 4.91 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.58 KB 2.58 KB 1.09 KB 1.09 KB NODE_PROD
react-reconciler.development.js +0.7% +0.7% 589.34 KB 593.37 KB 124.24 KB 125.09 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.5% 🔺+0.4% 79.47 KB 79.87 KB 23.4 KB 23.5 KB NODE_PROD

ReactDOM: size: 🔺+16.5%, gzip: 🔺+15.6%

Size changes (experimental)

Generated by 🚫 dangerJS against 815042f

@sizebot
Copy link

sizebot commented Nov 8, 2019

Details of bundled changes.

Comparing: 7785a52...815042f

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.production.min.js 🔺+0.5% 🔺+0.3% 118.87 KB 119.41 KB 37.28 KB 37.41 KB NODE_PROD
react-dom-test-utils.development.js +0.1% +0.2% 61.81 KB 61.9 KB 16.37 KB 16.4 KB UMD_DEV
ReactDOMTesting-profiling.js +0.4% +0.4% 403.12 KB 404.56 KB 73 KB 73.31 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 🔺+16.9% 🔺+15.9% 19.46 KB 22.75 KB 7.3 KB 8.46 KB NODE_PROD
react-dom-unstable-fizz.browser.development.js +1.9% +1.5% 4.81 KB 4.9 KB 1.61 KB 1.64 KB UMD_DEV
react-dom.profiling.min.js +0.4% +0.4% 122.72 KB 123.27 KB 38.42 KB 38.56 KB NODE_PROFILING
react-dom-test-utils.production.min.js 0.0% -0.0% 12.36 KB 12.36 KB 4.57 KB 4.57 KB UMD_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.1% 1.19 KB 1.19 KB 699 B 698 B UMD_PROD
react-dom-server.node.development.js +13.4% +12.9% 128.22 KB 145.36 KB 34.32 KB 38.76 KB NODE_DEV
react-dom-server.node.production.min.js 🔺+16.5% 🔺+15.9% 19.87 KB 23.16 KB 7.45 KB 8.63 KB NODE_PROD
ReactDOMForked-dev.js +0.5% +0.5% 994.24 KB 999.33 KB 220.62 KB 221.74 KB FB_WWW_DEV
ReactDOMForked-prod.js 🔺+0.3% 🔺+0.4% 418.73 KB 420.17 KB 75.21 KB 75.52 KB FB_WWW_PROD
react-dom-unstable-fizz.node.development.js +1.7% +1.5% 5.08 KB 5.16 KB 1.67 KB 1.69 KB NODE_DEV
react-dom.development.js +0.6% +0.6% 895.84 KB 901.06 KB 196.65 KB 197.86 KB UMD_DEV
ReactDOMForked-profiling.js +0.3% +0.4% 429.61 KB 431.05 KB 77.1 KB 77.42 KB FB_WWW_PROFILING
react-dom-server.browser.development.js +14.4% +13.8% 133.89 KB 153.19 KB 34.5 KB 39.27 KB UMD_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.2% 1.16 KB 1.16 KB 661 B 660 B NODE_PROD
react-dom.production.min.js 🔺+0.5% 🔺+0.3% 118.76 KB 119.3 KB 38.06 KB 38.19 KB UMD_PROD
react-dom-server.browser.production.min.js 🔺+16.9% 🔺+15.8% 19.55 KB 22.85 KB 7.33 KB 8.48 KB UMD_PROD
react-dom.profiling.min.js +0.4% +0.5% 122.47 KB 123.02 KB 39.23 KB 39.44 KB UMD_PROFILING
ReactDOMTesting-dev.js +0.5% +0.5% 947.31 KB 952.4 KB 210.86 KB 211.99 KB FB_WWW_DEV
react-dom.development.js +0.6% +0.6% 852.85 KB 857.78 KB 194.25 KB 195.4 KB NODE_DEV
ReactDOMTesting-prod.js 🔺+0.4% 🔺+0.4% 403.12 KB 404.56 KB 73 KB 73.31 KB FB_WWW_PROD
react-dom-server.browser.development.js +13.5% +13.0% 127 KB 144.15 KB 34.06 KB 38.51 KB NODE_DEV
ReactDOM-dev.js +0.5% +0.5% 994.24 KB 999.33 KB 220.62 KB 221.74 KB FB_WWW_DEV
ReactDOMServer-dev.js +13.4% +13.6% 136.77 KB 155.16 KB 34.86 KB 39.6 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.3% 🔺+0.4% 418.73 KB 420.17 KB 75.21 KB 75.52 KB FB_WWW_PROD
react-dom-test-utils.development.js +0.2% +0.2% 57.18 KB 57.27 KB 15.73 KB 15.76 KB NODE_DEV
ReactDOMServer-prod.js 🔺+13.4% 🔺+17.4% 46.58 KB 52.85 KB 10.87 KB 12.76 KB FB_WWW_PROD
react-dom-unstable-fizz.browser.development.js +2.0% +1.7% 4.33 KB 4.42 KB 1.51 KB 1.54 KB NODE_DEV
ReactDOM-profiling.js +0.3% +0.4% 429.61 KB 431.05 KB 77.1 KB 77.42 KB FB_WWW_PROFILING
react-dom-test-utils.production.min.js 0.0% 0.0% 12.22 KB 12.22 KB 4.5 KB 4.5 KB NODE_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 56.1 KB 56.1 KB 13.84 KB 13.84 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1 KB 1 KB 611 B 610 B NODE_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 9.98 KB 9.98 KB 3.36 KB 3.36 KB UMD_PROD
ReactTestUtils-dev.js +0.1% +0.2% 59.47 KB 59.56 KB 16.02 KB 16.05 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 53.2 KB 53.2 KB 13.65 KB 13.64 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 9.73 KB 9.73 KB 3.25 KB 3.25 KB NODE_PROD

react-debug-tools

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-debug-tools.production.min.js 🔺+5.3% 🔺+5.3% 5.63 KB 5.93 KB 2.15 KB 2.26 KB NODE_PROD
react-debug-tools.development.js +12.1% +11.8% 18.82 KB 21.11 KB 5.15 KB 5.76 KB NODE_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.4% +0.3% 648.42 KB 651.15 KB 139.3 KB 139.77 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+0.1% 🔺+0.1% 270.43 KB 270.75 KB 46.53 KB 46.56 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.1% +0.1% 282.34 KB 282.66 KB 48.73 KB 48.77 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.4% +0.3% 630.13 KB 632.86 KB 134.86 KB 135.33 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.1% 🔺+0.1% 262.42 KB 262.73 KB 45.02 KB 45.05 KB RN_OSS_PROD
ReactFabric-profiling.js +0.1% +0.1% 274.32 KB 274.64 KB 47.24 KB 47.27 KB RN_OSS_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.5% +0.4% 529.5 KB 532.16 KB 114.17 KB 114.64 KB NODE_DEV
react-art.production.min.js 🔺+0.3% 🔺+0.2% 71.75 KB 71.94 KB 21.58 KB 21.63 KB NODE_PROD
ReactART-dev.js +0.5% +0.4% 591.05 KB 593.77 KB 123.95 KB 124.45 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.1% 🔺+0.1% 248.27 KB 248.59 KB 41.95 KB 41.98 KB FB_WWW_PROD
react-art.development.js +0.5% +0.4% 624.73 KB 627.56 KB 131.84 KB 132.3 KB UMD_DEV
react-art.production.min.js 🔺+0.2% 🔺+0.2% 106.71 KB 106.9 KB 32.32 KB 32.38 KB UMD_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer-shallow.development.js +0.2% +0.3% 38.62 KB 38.71 KB 9.39 KB 9.42 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.0% 12.73 KB 12.73 KB 3.96 KB 3.96 KB UMD_PROD
react-test-renderer.development.js +0.5% +0.5% 536.33 KB 539.19 KB 115.61 KB 116.17 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.3% 🔺+0.3% 73.56 KB 73.76 KB 22.1 KB 22.17 KB NODE_PROD
ReactTestRenderer-dev.js +0.5% +0.5% 570.25 KB 573.16 KB 120.28 KB 120.86 KB FB_WWW_DEV
react-test-renderer.development.js +0.5% +0.5% 562.49 KB 565.53 KB 117.06 KB 117.6 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.3% 🔺+0.2% 73.73 KB 73.93 KB 22.42 KB 22.46 KB UMD_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.7% +0.7% 567.5 KB 571.53 KB 119.99 KB 120.83 KB NODE_DEV
react-reconciler-reflection.development.js +0.5% +0.6% 16.12 KB 16.21 KB 4.87 KB 4.9 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.5% 🔺+0.5% 76.5 KB 76.91 KB 22.66 KB 22.78 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.57 KB 2.57 KB 1.09 KB 1.08 KB NODE_PROD

ReactDOM: size: 0.0%, gzip: -0.0%

Size changes (stable)

Generated by 🚫 dangerJS against 815042f

Copy link
Collaborator

@acdlite acdlite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start! There are some cases missing but I'll chat to you about them in person

packages/react-dom/src/__tests__/ReactDOMHooks-test.js Outdated Show resolved Hide resolved
packages/react-dom/src/__tests__/ReactDOMHooks-test.js Outdated Show resolved Hide resolved
packages/react-dom/src/client/ToStringValue.js Outdated Show resolved Hide resolved
@@ -110,7 +110,7 @@ export function getValueForAttribute(
return expected === undefined ? undefined : null;
}
const value = node.getAttribute(name);
if (value === '' + (expected: any)) {
if (value === toString(expected)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if expected is the opaque object, this would throw the string error. toString doesn't convert the opaque object, which makes this just return value, which is what it should be anyway.

@@ -18,7 +19,11 @@ export function setAttribute(
attributeName: string,
attributeValue: string | TrustedValue,
) {
node.setAttribute(attributeName, (attributeValue: any));
if (attributeValue.$$typeof === REACT_OPAQUE_OBJECT_TYPE) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for updates, but need to handle similar case in setInitialDOMProperties, which is called during initial render.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think node.setAttribute is called during setInitialDOMProperties as well to set attributes:

setInitialDOMProperties -> setValueForProperty -> setAttribute/setAttributeNs

}

container.innerHTML =
'<div data-reactroot=""><div id="s_0">Child One</div><!--$!--><div>Fallback</div><!--/$--></div>';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also assert that React didn't replace the original DOM node. I would grab a reference to the divs here, then after you hydrate, make sure they match the hydrated ones (you can use a ref for that part, or grab them out of the container again). Then do the same after the update.

Scheduler.unstable_flushAll();
jest.runAllTimers();

expect(container.innerHTML).toMatchInlineSnapshot(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output is correct, but let's also test that there was only a single commit. You can do that with useEffect:

const id = useUniqueID();
useEffect(() => {
  Scheduler.unstable_yieldValue('Did commit');
});

I expect this test will fail; let's write the test first and then I can work with you on how to fix it.

root.render(<App />);
});

expect(container.innerHTML).toMatchInlineSnapshot(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you do expect(container).toMatchInlineSnapshot pretty-format will format the inner html as html (newlines and less escaping, closer to how you'd write html manually).

If you explicitly want innerHTML, you can do expect(wrap(container.innerHTML)).toMatchInlineSnapshot to at least avoid the inline escaping and wrapping quotes. wrap comes from jest-snapshot-serializer-raw and is already used in react-refresh's tests

@necolas necolas self-requested a review November 14, 2019 22:14
`);
});

it('generates unique ids for client render on good server markup', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snapshot below contains the server ids. Is this testing for preservation of ids from the server?

Copy link
Collaborator

@sebmarkbage sebmarkbage Jan 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea. I agree. Snapshot tests aren't a good way of testing these things. Especially when there are so many of them. I'd drop all the snapshots and replace them with more specific assertions.

We don't provide any guarantees that this is actually the format we will use. In fact, these IDs are way too likely to conflict with a manual user provided ID. So we probably should change the format and we might have to change it again.

A change in the format will yield a lot of manual work to verify if it's correct or not.

The tests also don't actually test the important properties that are useful to preserve regardless of which format we use. I.e. that a client generated ID can't overlap with a server generated ID. Does any of your tests fail if you just change it to be react_ prefixed on both server and client? Or what if you accidentally make remove the auto-incrementing, does it fail any tests?

Instead of asserting against a snapshot, you could assert on the DOM that the IDs match. E.g. find the divs and spans and assert that node.getAttribute('aria-labelledby') matches node.id and that they don't match the other's IDs. Also, write a test where the client generated IDs are not suppose to match existing server generated
content. E.g. by on the client adding a client-rendered pair. That's all that really matter. Not the exact format.

expect(Scheduler).toFlushAndYield([]);
jest.runAllTimers();

expect(divNode).toEqual(ref.current);
Copy link
Contributor

@necolas necolas Nov 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this assertion if we have the inline snapshot below?

Comment on lines 1256 to 1605
if (getIsHydrating()) {
return {
$$typeof: REACT_OPAQUE_OBJECT_TYPE,
_setId: () => {
setId(() => 'c_' + (clientId++).toString());
},
toString() {
throw Error(
'The object passed back from useUniqueID is meant to be passed through to ' +
'attributes only. Do not directly modify the output.',
);
},
};
} else {
return 'c_' + (clientId++).toString();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The body of this function looks identical to the mount function. Should we try to share it across mount and update?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The update shouldn’t need to create the initializer (normally). Can we short cut this and avoid creating the unnecessary closure and array?

return {
$$typeof: REACT_OPAQUE_OBJECT_TYPE,
_setId: () => {
setId(() => 'c_' + (clientId++).toString());
Copy link
Contributor

@Jessidhia Jessidhia Nov 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the same problem as most of the "userspace" solutions: this ID is unstable and depends on commit order which, especially in combination with Suspense and partial hydration, is not necessarily the same as tree order.

One thing library authors often asked for is a way of getting the "tree position" of the element, which is better and relatively stable, but still not perfect; it'll probably work better for hydration and will be globally unique, but might get reused if a component is swapped by another for example.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with this solution it doesn't matter whether the counter changes between server and client since react is able to patch it up properly during hydration which wasn't possible before.

This hook is exclusively for IDREF attributes. It's not a generic, unique ID of your component instance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so the point is it not being seen as a hydration error but just as something to be patched 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least this is how I understood it. There's an argument to be made about hydration warnings for attributes in the first place (e.g. <button disabled={!process.browser} /> but IDREFs seem like a safe place to start.

Copy link
Collaborator

@sebmarkbage sebmarkbage Dec 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not as simple as just patching. Partially because we don’t actually compare any props in hydration. But mostly because if you do it the naive way you may patch up one side but not the other side when partial hydration is being used.

This impl enforces that we force hydration of the things that needs patching on both sides.

We should have a test for that.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Dec 5, 2019

How about useOpaqueReference or even useOpaqueRef?

It’s not necessarily an ID in its implementation. It’s just some way of referencing one node in two places.

The connection to useRef is a bit confusing but technically correct. It’s a reference to a mutable imperative box.

I’ve thought about having this in React Native as a way to pass a native box around. So that I can pass a reference to a native View instance to native APIs. Eg getting a handle to a native View instance from another view’s View Manager on Native threads.

Unlike useRef, this kind of ref’s current value would only be accessible from native code.

That’s very similar to how you can think about this use case too.

It’s a bit strange since you might expect to pass the target side like <div ref={opaqueRef} /> instead of <div id={opaqueRef} />. Maybe we should even make that work. Requires a bit more code and special logic to do that though. It also means that the value can’t literally be a string since it would conflict with string refs.

toString() {
throw Error(
'The object passed back from useUniqueID is meant to be passed through to ' +
'attributes only. Do not directly modify the output.',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does “don’t modify the output” mean? I’m just reading the value. Not modifying it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an invariant since our error minification pipeline doesn't support error constructors, yet.

) {
node.setAttribute(attributeName, (attributeValue: any));
if (
typeof attributeValue === 'object' &&
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 probably React’s hottest path that we’re making slower (and not inlinable). Is this the best place we can do this? Where does TrustedValue do its check?

@@ -260,6 +264,17 @@ function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
return value;
}

function useOpaqueReference(): string | IdObject {
const hook = nextHook();
const value = hook === null ? 'FAKE_ID' : hook.memoizedState;
Copy link
Collaborator

@sebmarkbage sebmarkbage Jan 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name "FAKE_ID" doesn't say much and is kind of confusing if you do end up seeing it. What's Fake about it? Can I search for it? It also doesn't apply as a string when in React Native for example. Let's just leave it as undefined if it's not there.

I believe this can also cause an error if you inspect a hydrated component since toString and valueOf will throw. We should probably detect if it's a REACT_OPAQUE_OBJECT_TYPE and do something special. Perhaps just leaving it as undefined as well.

You can add tests to: https://github.com/facebook/react/tree/9fe1031244903e442de179821f1d383a9f2a59f2/packages/react-debug-tools/src/__tests__

const container = document.createElement('div');

container.innerHTML =
'<div data-reactroot=""><div id="s_0">Child One</div><!--$!--><div>Fallback</div><!--/$--></div>';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use ReactDOMServer.renderToString(...) to render this string so that you don't have to hard code the format.

Example:

let finalHTML = ReactDOMServer.renderToString(<App />);

@@ -60,7 +86,8 @@ if (enableTrustedTypesIntegration && typeof trustedTypes !== 'undefined') {
trustedTypes.isScript(value) ||
trustedTypes.isScriptURL(value) ||
/* TrustedURLs are deprecated and will be removed soon: https://github.com/WICG/trusted-types/pull/204 */
(trustedTypes.isURL && trustedTypes.isURL(value)))
(trustedTypes.isURL && trustedTypes.isURL(value)) ||
value.$$typeof === REACT_OPAQUE_OBJECT_TYPE)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right. This will be toString:ed by the DOM and throw an error anyway. No need to add a special case, is there? Just let it fail below.

value !== null &&
typeof value === 'object' &&
value.$$typeof === REACT_OPAQUE_OBJECT_TYPE &&
typeof value._setId === 'function'
Copy link
Collaborator

@sebmarkbage sebmarkbage Jan 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need this extra check. You've asserted the type. Just cast it through let obj: IdObject = (value: any) below to satisfy Flow.

value._setId();
return '';
} else {
return toStringOrTrustedType(value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point to collocate these was so that you could use the same type check to quickly determine if this is not an object which is the common case. However, by calling this second function, you have to check the type again in the other function. Leading to two checks instead of one, for the common case.

You should inline this here so that you can avoid extra type checks. However, the bigger question is why you need two functions at all.

@@ -44,14 +45,39 @@ export opaque type TrustedValue: {toString(): string, valueOf(): string} = {
valueOf(): string,
};

export function evaluateToStringOrTrustedType(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be its own function? Can't it just be the same as toStringOrTrustedType?

toStringOrTrustedType is called in two other places.

This one seems like you might have missed: https://github.com/facebook/react/pull/17322/files#diff-214d5116dd8b5a7d184cdf5b2160f94cR183

Seems like it should also trigger this path.

The other one is here but I think that's actually already the wrong place to do this and it shouldn't toString this at all since the initial rendering doesn't:

toStringOrTrustedType(nextHtml),

But it doesn't hurt that we check for REACT_OPAQUE_OBJECT_TYPE. Even though the existing call is a bug in the first place.

That way we need less code overall.

@@ -110,7 +114,7 @@ export function getValueForAttribute(
return expected === undefined ? undefined : null;
}
const value = node.getAttribute(name);
if (value === '' + (expected: any)) {
if (value === toStringOrTrustedType(getToStringValue(expected))) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call site seems like it breaks with trusted types already.

But this fix doesn't seem right. Why call getToStringValue? Why would you need to ignore function values or symbol? getAttribute should never return those types.

toStringOrTrustedType also doesn't seem like the right fix neither.

For trusted types, the server generated string won't match the object on the client, so this wouldn't end up matching. Causing a hydration error. This will need to dig into the trusted type and inspect the value to see if it's equivalent.

For REACT_OPAQUE_OBJECT_TYPE the returned object will never match. So that will also be a hydration error.

It seems to me that this needs to just perform some more custom matching logic instead of trying to use the toStringOrTrustedType helper.

@@ -197,6 +198,9 @@ if (__DEV__) {
if (didWarnInvalidHydration) {
return;
}
if (clientValue && clientValue.$$typeof === REACT_OPAQUE_OBJECT_TYPE) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the wrong place to do this check. If we get here it's because we already thought that there was an error. If you do the check in getValueForAttribute instead, then we don't have to treat it as an error in the first place.

const [id, setId] = mountState(() => {
if (getIsHydrating()) {
return {
$$typeof: REACT_OPAQUE_OBJECT_TYPE,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than its creation, this object is only ever referenced in ReactDOM. That's a good thing because on other platforms like the React Native use case, this object will look very different and have a very different signature.

To do that, though, we need to make the creation of this object something platform specific. Let's make a HostConfig method to create this object and only implement it in ReactDOM.

For React Native, we should make that method throw since this feature is not yet supported there. Later we could add some specific implementation.

You probably need to pass setId in as an argument to the HostConfig method.

},
};
} else {
return 'c_' + (clientId++).toString();
Copy link
Collaborator

@sebmarkbage sebmarkbage Jan 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also move to ReactDOM since it's specific to how an "Opaque Reference" is constructed in the DOM.

Also, don't call toString(). Rely on + '' casting instead like we do elsewhere. .toString() becomes a dynamic look up which can be monkey patched by Number.prototype. It's more bytes and less efficient.

Actually, scratch that, let's use toString(36) instead to save some bytes in memory on the ID. On the client it doesn't matter as much but nice on the server so might as well use it for consistency.

</div>
</div>
`);
});
Copy link
Collaborator

@sebmarkbage sebmarkbage Jan 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a similar test where Child One is in a Suspense boundary that is still suspended. I.e. if we're only partially hydrated. Then we do an update that shows the second Child. This should suspend and wait for the suspense boundary to resolve before letting the update through.

It should not leave them inconsistent.

That test would ensure that we don't forget about this case and try to implement the "patching strategy approach" in the future. That test is really the main driver for this particular implementation strategy so we should ensure we test for it.

https://github.com/facebook/react/pull/17322/files#r354445736

This was referenced Jun 23, 2024
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.

None yet