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 useOpaqueReference Hook #17322

Open
wants to merge 20 commits into
base: master
from
Open

Add useOpaqueReference Hook #17322

wants to merge 20 commits into from

Conversation

@lunaruan
Copy link
Contributor

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 useUniqueId 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

This comment has been minimized.

Copy link

codesandbox 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 1c30aae:

Sandbox Source
friendly-mendeleev-0z4xm Configuration
@sizebot

This comment has been minimized.

Copy link

sizebot commented Nov 8, 2019

Details of bundled changes.

Comparing: 3e09677...1c30aae

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +0.4% +0.5% 123.65 KB 124.2 KB 38.89 KB 39.07 KB NODE_PROFILING
react-dom-server.browser.development.js +0.1% +0.1% 137.71 KB 137.85 KB 36.61 KB 36.65 KB UMD_DEV
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.2% 20.45 KB 20.52 KB 7.5 KB 7.51 KB UMD_PROD
react-dom-test-utils.development.js 0.0% -0.0% 54.5 KB 54.5 KB 15.32 KB 15.31 KB UMD_DEV
ReactDOMServer-prod.js 🔺+0.2% 🔺+0.3% 49 KB 49.11 KB 11.19 KB 11.22 KB FB_WWW_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.88 KB 3.88 KB 1.55 KB 1.55 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.1% 11.18 KB 11.18 KB 4.15 KB 4.15 KB UMD_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.4% 1.21 KB 1.21 KB 711 B 708 B UMD_PROD
react-dom-test-utils.development.js 0.0% -0.0% 52.77 KB 52.77 KB 14.99 KB 14.99 KB NODE_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.2% 3.71 KB 3.71 KB 1.5 KB 1.5 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 10.95 KB 10.95 KB 4.09 KB 4.09 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.5% 1.05 KB 1.05 KB 642 B 639 B NODE_PROD
react-dom.development.js +0.3% +0.2% 951.51 KB 954.17 KB 215.43 KB 215.91 KB UMD_DEV
react-dom.production.min.js 🔺+0.5% 🔺+0.5% 119.68 KB 120.22 KB 38.5 KB 38.69 KB UMD_PROD
react-dom.profiling.min.js +0.4% +0.4% 123.42 KB 123.96 KB 39.66 KB 39.82 KB UMD_PROFILING
react-dom.development.js +0.3% +0.2% 945.58 KB 948.24 KB 213.77 KB 214.26 KB NODE_DEV
react-dom-server.node.development.js +0.1% +0.1% 134.76 KB 134.89 KB 35.82 KB 35.86 KB NODE_DEV
react-dom.production.min.js 🔺+0.5% 🔺+0.4% 119.77 KB 120.32 KB 37.77 KB 37.93 KB NODE_PROD
react-dom-server.node.production.min.js 🔺+0.3% 🔺+0.4% 20.79 KB 20.86 KB 7.63 KB 7.66 KB NODE_PROD
react-dom-server.browser.development.js +0.1% +0.1% 133.65 KB 133.78 KB 35.58 KB 35.63 KB NODE_DEV
ReactDOM-dev.js +0.3% +0.2% 973.5 KB 976.3 KB 216.64 KB 217.15 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.4% 20.38 KB 20.45 KB 7.47 KB 7.5 KB NODE_PROD
ReactDOM-prod.js 🔺+0.4% 🔺+0.4% 394.64 KB 396.05 KB 72.3 KB 72.61 KB FB_WWW_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.91 KB 60.91 KB 16.07 KB 16.07 KB UMD_DEV
ReactDOM-profiling.js +0.3% +0.4% 405.94 KB 407.35 KB 74.42 KB 74.74 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.23 KB 10.23 KB 3.48 KB 3.47 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.61 KB 60.61 KB 16 KB 15.99 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% -0.2% 4.42 KB 4.42 KB 1.65 KB 1.65 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 9.97 KB 9.97 KB 3.37 KB 3.37 KB NODE_PROD
ReactDOMServer-dev.js +0.1% +0.1% 139.36 KB 139.49 KB 35.43 KB 35.47 KB FB_WWW_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.4% 1.21 KB 1.21 KB 698 B 695 B 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.3% +0.3% 616.22 KB 618.27 KB 131.31 KB 131.66 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.5% 🔺+0.6% 71.5 KB 71.87 KB 21.91 KB 22.03 KB UMD_PROD
react-test-renderer-shallow.development.js +0.5% +0.4% 37.83 KB 38.03 KB 9.81 KB 9.85 KB UMD_DEV
react-test-renderer-shallow.production.min.js 🔺+0.8% 🔺+0.7% 11.66 KB 11.75 KB 3.6 KB 3.63 KB UMD_PROD
react-test-renderer-shallow.development.js +0.6% +0.5% 32.37 KB 32.57 KB 8.5 KB 8.54 KB NODE_DEV
react-test-renderer-shallow.production.min.js 🔺+0.8% 🔺+0.5% 11.79 KB 11.89 KB 3.71 KB 3.73 KB NODE_PROD
react-test-renderer.development.js +0.3% +0.3% 611.49 KB 613.54 KB 130.13 KB 130.47 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.5% 🔺+0.5% 71.19 KB 71.57 KB 21.53 KB 21.63 KB NODE_PROD
ReactShallowRenderer-dev.js +0.6% +0.5% 34.38 KB 34.59 KB 8.46 KB 8.51 KB FB_WWW_DEV
ReactTestRenderer-dev.js +0.3% +0.2% 627.33 KB 629.49 KB 130.91 KB 131.24 KB FB_WWW_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.3% +0.2% 744.18 KB 746.27 KB 157.71 KB 158.03 KB RN_OSS_DEV
ReactFabric-dev.js +0.3% +0.2% 734.75 KB 736.83 KB 155.46 KB 155.78 KB RN_OSS_DEV
ReactNativeRenderer-dev.js +0.3% +0.2% 744.36 KB 746.45 KB 157.8 KB 158.12 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.5% 🔺+0.6% 265.58 KB 266.79 KB 45.72 KB 46 KB RN_OSS_PROD
ReactNativeRenderer-prod.js 🔺+0.4% 🔺+0.6% 273.57 KB 274.78 KB 47.06 KB 47.35 KB RN_FB_PROD
ReactFabric-profiling.js +0.4% +0.6% 276.71 KB 277.92 KB 47.85 KB 48.12 KB RN_OSS_PROFILING
ReactNativeRenderer-profiling.js +0.4% +0.6% 284.74 KB 285.95 KB 49.21 KB 49.49 KB RN_FB_PROFILING
ReactNativeRenderer-prod.js 🔺+0.4% 🔺+0.6% 273.18 KB 274.39 KB 46.99 KB 47.27 KB RN_OSS_PROD
ReactFabric-dev.js +0.3% +0.2% 734.93 KB 737.02 KB 155.55 KB 155.86 KB RN_FB_DEV
ReactNativeRenderer-profiling.js +0.4% +0.6% 284.35 KB 285.57 KB 49.14 KB 49.42 KB RN_OSS_PROFILING
ReactFabric-prod.js 🔺+0.5% 🔺+0.6% 265.93 KB 267.14 KB 45.79 KB 46.07 KB RN_FB_PROD
ReactFabric-profiling.js +0.4% +0.6% 277.06 KB 278.27 KB 47.91 KB 48.19 KB RN_FB_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +0.4% +0.2% 615.73 KB 617.9 KB 128.38 KB 128.7 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.4% 🔺+0.6% 236.25 KB 237.18 KB 39.92 KB 40.16 KB FB_WWW_PROD
react-art.development.js +0.3% +0.2% 671.19 KB 673.24 KB 145.69 KB 146.02 KB UMD_DEV
react-art.production.min.js 🔺+0.3% 🔺+0.3% 106.83 KB 107.2 KB 32.5 KB 32.59 KB UMD_PROD
react-art.development.js +0.3% +0.3% 601.87 KB 603.92 KB 128.28 KB 128.62 KB NODE_DEV
react-art.production.min.js 🔺+0.5% 🔺+0.5% 71.78 KB 72.16 KB 21.65 KB 21.76 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-persistent.development.js +0.3% +0.3% 600.32 KB 602.37 KB 126.34 KB 126.68 KB NODE_DEV
react-reconciler-reflection.development.js 0.0% -0.0% 19.27 KB 19.27 KB 6.33 KB 6.33 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.5% 🔺+0.6% 72.07 KB 72.44 KB 21.24 KB 21.37 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.2% 2.86 KB 2.86 KB 1.24 KB 1.24 KB NODE_PROD
react-reconciler.development.js +0.3% +0.3% 602.87 KB 604.92 KB 127.45 KB 127.79 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.5% 🔺+0.7% 74.76 KB 75.14 KB 21.91 KB 22.06 KB NODE_PROD

ReactDOM: size: 🔺+0.3%, gzip: 🔺+0.2%

Size changes (experimental)

Generated by 🚫 dangerJS against 1c30aae

@sizebot

This comment has been minimized.

Copy link

sizebot commented Nov 8, 2019

Details of bundled changes.

Comparing: 3e09677...1c30aae

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.production.min.js 🔺+0.4% 🔺+0.3% 104.33 KB 104.7 KB 31.8 KB 31.91 KB UMD_PROD
react-art.development.js +0.3% +0.2% 671.17 KB 673.22 KB 145.69 KB 146.02 KB UMD_DEV
react-art.development.js +0.3% +0.3% 601.85 KB 603.9 KB 128.28 KB 128.61 KB NODE_DEV
react-art.production.min.js 🔺+0.5% 🔺+0.5% 69.33 KB 69.7 KB 20.97 KB 21.08 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-persistent.production.min.js 🔺+0.5% 🔺+0.6% 72.05 KB 72.43 KB 21.23 KB 21.36 KB NODE_PROD
react-reconciler.development.js +0.3% +0.3% 602.86 KB 604.91 KB 127.45 KB 127.79 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.5% 🔺+0.6% 72.04 KB 72.42 KB 21.22 KB 21.36 KB NODE_PROD
react-reconciler-reflection.development.js 0.0% -0.0% 19.25 KB 19.25 KB 6.33 KB 6.33 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% -0.2% 2.85 KB 2.85 KB 1.24 KB 1.23 KB NODE_PROD
react-reconciler-persistent.development.js +0.3% +0.3% 600.31 KB 602.36 KB 126.33 KB 126.67 KB NODE_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.3% +0.2% 734.73 KB 736.82 KB 155.45 KB 155.77 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.5% 🔺+0.6% 265.57 KB 266.78 KB 45.71 KB 45.99 KB RN_OSS_PROD
ReactNativeRenderer-dev.js +0.3% +0.2% 744.17 KB 746.26 KB 157.7 KB 158.02 KB RN_OSS_DEV
ReactFabric-profiling.js +0.4% +0.6% 276.7 KB 277.91 KB 47.84 KB 48.11 KB RN_OSS_PROFILING
ReactNativeRenderer-prod.js 🔺+0.4% 🔺+0.6% 273.17 KB 274.38 KB 46.98 KB 47.26 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.4% +0.6% 284.34 KB 285.55 KB 49.13 KB 49.41 KB RN_OSS_PROFILING

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.4% 19.92 KB 19.99 KB 7.39 KB 7.42 KB NODE_PROD
react-dom-test-utils.development.js 0.0% -0.0% 52.76 KB 52.76 KB 14.99 KB 14.99 KB NODE_DEV
react-dom.production.min.js 🔺+0.5% 🔺+0.4% 115.84 KB 116.39 KB 37.38 KB 37.54 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.59 KB 60.59 KB 15.99 KB 15.99 KB NODE_DEV
react-dom.profiling.min.js +0.5% +0.5% 119.47 KB 120.02 KB 38.53 KB 38.73 KB UMD_PROFILING
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 9.96 KB 9.96 KB 3.37 KB 3.36 KB NODE_PROD
react-dom.development.js +0.3% +0.2% 945.56 KB 948.22 KB 213.75 KB 214.24 KB NODE_DEV
react-dom.production.min.js 🔺+0.5% 🔺+0.5% 115.9 KB 116.46 KB 36.7 KB 36.89 KB NODE_PROD
react-dom.profiling.min.js +0.5% +0.4% 119.67 KB 120.22 KB 37.83 KB 38 KB NODE_PROFILING
react-dom-unstable-fizz.browser.development.js 0.0% -0.2% 3.87 KB 3.87 KB 1.54 KB 1.54 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.2 KB 1.2 KB 703 B 701 B UMD_PROD
react-dom-server.browser.development.js +0.1% +0.1% 137.69 KB 137.82 KB 36.61 KB 36.65 KB UMD_DEV
react-dom-server.browser.production.min.js 🔺+0.3% 🔺+0.5% 19.99 KB 20.06 KB 7.4 KB 7.44 KB UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.2% 3.7 KB 3.7 KB 1.5 KB 1.49 KB NODE_DEV
react-dom-test-utils.development.js 0.0% -0.0% 54.48 KB 54.48 KB 15.31 KB 15.31 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.5% 1.04 KB 1.04 KB 634 B 631 B NODE_PROD
react-dom-test-utils.production.min.js 0.0% -0.1% 11.17 KB 11.17 KB 4.14 KB 4.14 KB UMD_PROD
react-dom-server.browser.development.js +0.1% +0.1% 133.62 KB 133.76 KB 35.58 KB 35.63 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 10.94 KB 10.94 KB 4.08 KB 4.08 KB NODE_PROD
react-dom-unstable-fizz.node.development.js 0.0% -0.2% 4.4 KB 4.4 KB 1.64 KB 1.64 KB NODE_DEV
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.89 KB 60.89 KB 16.06 KB 16.06 KB UMD_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.3% 1.2 KB 1.2 KB 689 B 687 B NODE_PROD
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.22 KB 10.22 KB 3.47 KB 3.46 KB UMD_PROD
react-dom-server.node.development.js +0.1% +0.1% 134.73 KB 134.87 KB 35.81 KB 35.86 KB NODE_DEV
react-dom.development.js +0.3% +0.2% 951.49 KB 954.15 KB 215.4 KB 215.89 KB UMD_DEV
react-dom-server.node.production.min.js 🔺+0.3% 🔺+0.4% 20.33 KB 20.4 KB 7.55 KB 7.58 KB NODE_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.5% +0.4% 37.81 KB 38.01 KB 9.8 KB 9.85 KB UMD_DEV
react-test-renderer-shallow.production.min.js 🔺+0.8% 🔺+0.7% 11.64 KB 11.74 KB 3.59 KB 3.62 KB UMD_PROD
react-test-renderer-shallow.development.js +0.6% +0.5% 32.35 KB 32.55 KB 8.49 KB 8.53 KB NODE_DEV
react-test-renderer-shallow.production.min.js 🔺+0.8% 🔺+0.5% 11.78 KB 11.88 KB 3.7 KB 3.72 KB NODE_PROD
react-test-renderer.development.js +0.3% +0.3% 616.2 KB 618.25 KB 131.3 KB 131.64 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.5% 🔺+0.5% 71.48 KB 71.85 KB 21.89 KB 22.01 KB UMD_PROD
react-test-renderer.development.js +0.3% +0.3% 611.46 KB 613.51 KB 130.12 KB 130.46 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.5% 🔺+0.5% 71.17 KB 71.54 KB 21.51 KB 21.61 KB NODE_PROD

ReactDOM: size: 🔺+0.5%, gzip: 🔺+0.4%

Size changes (stable)

Generated by 🚫 dangerJS against 1c30aae

Copy link
Member

acdlite left a comment

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

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)) {

This comment has been minimized.

Copy link
@acdlite

acdlite Nov 9, 2019

Member

Why this change?

This comment has been minimized.

Copy link
@lunaruan

lunaruan Nov 11, 2019

Author Contributor

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) {

This comment has been minimized.

Copy link
@acdlite

acdlite Nov 9, 2019

Member

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

This comment has been minimized.

Copy link
@lunaruan

lunaruan Nov 11, 2019

Author Contributor

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

setInitialDOMProperties -> setValueForProperty -> setAttribute/setAttributeNs

Luna Ruan added 2 commits Nov 11, 2019
Luna Ruan
}

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

This comment has been minimized.

Copy link
@acdlite

acdlite Nov 12, 2019

Member

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(

This comment has been minimized.

Copy link
@acdlite

acdlite Nov 12, 2019

Member

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.

@diegohaz

This comment has been minimized.

Copy link

diegohaz commented Nov 12, 2019

I’d expect that calling it multiple times in the same component would produce the same ID so you would be able to use it in different custom hooks and they would point to the same ID. But it seems that calling useUniqueID multiple times in the same component will generate multiple IDs. What’s special about this that can’t be done outside React?

Luna Ruan added 2 commits Nov 12, 2019
Luna Ruan
Luna Ruan
root.render(<App />);
});

expect(container.innerHTML).toMatchInlineSnapshot(

This comment has been minimized.

Copy link
@SimenB

SimenB Nov 13, 2019

Contributor

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

Luna Ruan and others added 4 commits Nov 13, 2019
Luna Ruan
Luna Ruan
Luna Ruan
@necolas necolas self-requested a review Nov 14, 2019
`);
});

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

This comment has been minimized.

Copy link
@necolas

necolas Nov 15, 2019

Contributor

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

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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);

This comment has been minimized.

Copy link
@necolas

necolas Nov 15, 2019

Contributor

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

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();
}
Comment on lines 1256 to 1271

This comment has been minimized.

Copy link
@necolas

necolas Nov 15, 2019

Contributor

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

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Dec 5, 2019

Member

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());

This comment has been minimized.

Copy link
@Jessidhia

Jessidhia Nov 18, 2019

Collaborator

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.

This comment has been minimized.

Copy link
@eps1lon

eps1lon Nov 18, 2019

Contributor

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.

This comment has been minimized.

Copy link
@Jessidhia

Jessidhia Nov 18, 2019

Collaborator

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

This comment has been minimized.

Copy link
@eps1lon

eps1lon Nov 18, 2019

Contributor

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.

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Dec 5, 2019

Member

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

This comment has been minimized.

Copy link
Member

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.',

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Dec 5, 2019

Member

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

This comment has been minimized.

Copy link
@acdlite

acdlite Dec 18, 2019

Member

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' &&

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Dec 5, 2019

Member

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?

@hzhu hzhu mentioned this pull request Dec 19, 2019
3 of 4 tasks complete
@lunaruan lunaruan changed the title Add useUniqueId Hook Add useOpaqueReference Hook Jan 2, 2020
@@ -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;

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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>';

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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)

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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'

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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);

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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(

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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))) {

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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) {

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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,

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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();

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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>
`);
});

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

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

let serverId: number = 0;

function useOpaqueReference(): string | IdObject {
return 's_' + (serverId++).toString();

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 3, 2020

Member

Let's use toString(36) here. It'll save us a couple of bytes when we have many IDs.

Copy link
Member

sebmarkbage left a comment

If you have more than one ReactDOM on the page, then there's risk of collision between the generated IDs. I'm not sure if we should consider this case.

Similarly, if you load additional server rendered pieces, those might conflict with previously server rendered pieces of the page.

if (getIsHydrating()) {
return {
$$typeof: REACT_OPAQUE_OBJECT_TYPE,
_setId: () => {

This comment has been minimized.

Copy link
@sebmarkbage

sebmarkbage Jan 4, 2020

Member

It is possible for this function to be called more than once if the object is referenced multiple times. This would eventually get rendered but unnecessarily queue up many updates and waste IDs that won't get used. We might want to dedupe so that if it's called more than once, we don't generate a new id each time.

@sebmarkbage

This comment has been minimized.

Copy link
Member

sebmarkbage commented Jan 4, 2020

If we land #17774 then this will need to be rebased on it.

It removes any brand checks in React. This makes it a bit awkward to add just for this case.

An alternative approach could be that toString/valueOf calls setId when accessed and returning empty string. That way it happens automatically by the DOM calling these methods. Then we can add a DEV error if these are called in a different context than React calling them. E.g. we can detect this object in DEV only and do the special case but then that way there's no special brand checking in prod.

@mike-marcacci

This comment has been minimized.

Copy link

mike-marcacci commented Jan 10, 2020

I was just made aware of this PR which addresses the same problem I identified in #15435. However, my approach is a bit different in that I propose that an ID should be based on the component's path in the react tree. My reasoning is that this is the only thing that is stable across the server and client and not dependent on rendering order/etc.

I would be interested to hear any thoughts on why the approach in this PR might be superior to my proposed strategy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.