@samitouri / QOS-React / commits / efe4121ee2

Add : to beginning and end of every useId (#23360)

The ids generated by useId are unique per React root. You can create additional ids by concatenating them with locally unique strings. To support this pattern, no id will ever be a subset of another id. We achieve this by adding a special character to the beginning and end. We use a colon (":") because it's uncommon — even if you don't prefix the ids using the `identifierPrefix` option, collisions are unlikely. One downside of a colon is that it's not a valid character in DOM selectors, like `querySelectorAll`. We think this is probably fine because it's not a common use case in React, and there are workarounds or alternative solutions. But we're open to reconsidering this in the future if there's a compelling argument.

Andrew Clark committed Feb 24, 2022 at 20:07 UTC efe4121ee218099539523a713272edadbaafca2a
5 files changed +25 -21
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+1 -1
@@ -614,7 +614,7 @@ describe('ReactHooksInspectionIntegration', () => {
614 expect(tree[0].id).toEqual(0);
615 expect(tree[0].isStateEditable).toEqual(false);
616 expect(tree[0].name).toEqual('Id');
617 - expect(String(tree[0].value).startsWith('r:')).toBe(true);
617 + expect(String(tree[0].value).startsWith(':r')).toBe(true);
618
619 expect(tree[1]).toEqual({
620 id: 1,
packages/react-dom/src/__tests__/ReactDOMUseId-test.js
+15 -11
@@ -93,7 +93,11 @@ describe('useId', () => {
93 }
94
95 function normalizeTreeIdForTesting(id) {
96 - const [serverClientPrefix, base32, hookIndex] = id.split(':');
96 + const result = id.match(/:(R|r)(.*):(([0-9]*):)?/);
97 + if (result === undefined) {
98 + throw new Error('Invalid id format');
99 + }
100 + const [, serverClientPrefix, base32, hookIndex] = result;
101 if (serverClientPrefix.endsWith('r')) {
102 // Client ids aren't stable. For testing purposes, strip out the counter.
103 return (
@@ -278,7 +282,7 @@ describe('useId', () => {
282 // 'R:' prefix, and the first character after that, which may not correspond
283 // to a complete set of 5 bits.
284 //
281 - // Example: R:clalalalalalalala...
285 + // Example: :Rclalalalalalalala...:
286 //
287 // We can use this pattern to test large ids that exceed the bitwise
288 // safe range (32 bits). The algorithm should theoretically support ids
@@ -313,8 +317,8 @@ describe('useId', () => {
317
318 // Confirm that every id matches the expected pattern
319 for (let i = 0; i < divs.length; i++) {
316 - // Example: R:clalalalalalalala...
317 - expect(divs[i].id).toMatch(/^R:.(((al)*a?)((la)*l?))*$/);
320 + // Example: :Rclalalalalalalala...:
321 + expect(divs[i].id).toMatch(/^:R.(((al)*a?)((la)*l?))*:$/);
322 }
323 });
324
@@ -338,7 +342,7 @@ describe('useId', () => {
342 <div
343 id="container"
344 >
341 - R:0, R:0:1, R:0:2
345 + :R0:, :R0:1:, :R0:2:
346 <!-- -->
347 </div>
348 `);
@@ -364,7 +368,7 @@ describe('useId', () => {
368 <div
369 id="container"
370 >
367 - R:0
371 + :R0:
372 <!-- -->
373 </div>
374 `);
@@ -603,10 +607,10 @@ describe('useId', () => {
607 id="container"
608 >
609 <div>
606 - custom-prefix-R:1
610 + :custom-prefix-R1:
611 </div>
612 <div>
609 - custom-prefix-R:2
613 + :custom-prefix-R2:
614 </div>
615 </div>
616 `);
@@ -620,13 +624,13 @@ describe('useId', () => {
624 id="container"
625 >
626 <div>
623 - custom-prefix-R:1
627 + :custom-prefix-R1:
628 </div>
629 <div>
626 - custom-prefix-R:2
630 + :custom-prefix-R2:
631 </div>
632 <div>
629 - custom-prefix-r:0
633 + :custom-prefix-r0:
634 </div>
635 </div>
636 `);
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+3 -3
@@ -130,7 +130,7 @@ export function createResponseState(
130 placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
131 segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
132 boundaryPrefix: idPrefix + 'B:',
133 - idPrefix: idPrefix + 'R:',
133 + idPrefix: idPrefix,
134 nextSuspenseID: 0,
135 sentCompleteSegmentFunction: false,
136 sentCompleteBoundaryFunction: false,
@@ -242,13 +242,13 @@ export function makeId(
242 ): string {
243 const idPrefix = responseState.idPrefix;
244
245 - let id = idPrefix + treeId;
245 + let id = ':' + idPrefix + 'R' + treeId + ':';
246
247 // Unless this is the first id at this level, append a number at the end
248 // that represents the position of this useId hook among all the useId
249 // hooks for this fiber.
250 if (localId > 0) {
251 - id += ':' + localId.toString(32);
251 + id += localId.toString(32) + ':';
252 }
253
254 return id;
packages/react-reconciler/src/ReactFiberHooks.new.js
+3 -3
@@ -2072,19 +2072,19 @@ function mountId(): string {
2072 const treeId = getTreeId();
2073
2074 // Use a captial R prefix for server-generated ids.
2075 - id = identifierPrefix + 'R:' + treeId;
2075 + id = ':' + identifierPrefix + 'R' + treeId + ':';
2076
2077 // Unless this is the first id at this level, append a number at the end
2078 // that represents the position of this useId hook among all the useId
2079 // hooks for this fiber.
2080 const localId = localIdCounter++;
2081 if (localId > 0) {
2082 - id += ':' + localId.toString(32);
2082 + id += localId.toString(32) + ':';
2083 }
2084 } else {
2085 // Use a lowercase r prefix for client-generated ids.
2086 const globalClientId = globalClientIdCounter++;
2087 - id = identifierPrefix + 'r:' + globalClientId.toString(32);
2087 + id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
2088 }
2089
2090 hook.memoizedState = id;
packages/react-reconciler/src/ReactFiberHooks.old.js
+3 -3
@@ -2072,19 +2072,19 @@ function mountId(): string {
2072 const treeId = getTreeId();
2073
2074 // Use a captial R prefix for server-generated ids.
2075 - id = identifierPrefix + 'R:' + treeId;
2075 + id = ':' + identifierPrefix + 'R' + treeId + ':';
2076
2077 // Unless this is the first id at this level, append a number at the end
2078 // that represents the position of this useId hook among all the useId
2079 // hooks for this fiber.
2080 const localId = localIdCounter++;
2081 if (localId > 0) {
2082 - id += ':' + localId.toString(32);
2082 + id += localId.toString(32) + ':';
2083 }
2084 } else {
2085 // Use a lowercase r prefix for client-generated ids.
2086 const globalClientId = globalClientIdCounter++;
2087 - id = identifierPrefix + 'r:' + globalClientId.toString(32);
2087 + id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
2088 }
2089
2090 hook.memoizedState = id;