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;