Skip to content

Commit efe4121

Browse files
authored
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.
1 parent 42f15b3 commit efe4121

File tree

5 files changed

+25
-21
lines changed

5 files changed

+25
-21
lines changed

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ describe('ReactHooksInspectionIntegration', () => {
614614
expect(tree[0].id).toEqual(0);
615615
expect(tree[0].isStateEditable).toEqual(false);
616616
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);
618618

619619
expect(tree[1]).toEqual({
620620
id: 1,

packages/react-dom/src/__tests__/ReactDOMUseId-test.js

+15-11
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ describe('useId', () => {
9393
}
9494

9595
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;
97101
if (serverClientPrefix.endsWith('r')) {
98102
// Client ids aren't stable. For testing purposes, strip out the counter.
99103
return (
@@ -278,7 +282,7 @@ describe('useId', () => {
278282
// 'R:' prefix, and the first character after that, which may not correspond
279283
// to a complete set of 5 bits.
280284
//
281-
// Example: R:clalalalalalalala...
285+
// Example: :Rclalalalalalalala...:
282286
//
283287
// We can use this pattern to test large ids that exceed the bitwise
284288
// safe range (32 bits). The algorithm should theoretically support ids
@@ -313,8 +317,8 @@ describe('useId', () => {
313317

314318
// Confirm that every id matches the expected pattern
315319
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?))*:$/);
318322
}
319323
});
320324

@@ -338,7 +342,7 @@ describe('useId', () => {
338342
<div
339343
id="container"
340344
>
341-
R:0, R:0:1, R:0:2
345+
:R0:, :R0:1:, :R0:2:
342346
<!-- -->
343347
</div>
344348
`);
@@ -364,7 +368,7 @@ describe('useId', () => {
364368
<div
365369
id="container"
366370
>
367-
R:0
371+
:R0:
368372
<!-- -->
369373
</div>
370374
`);
@@ -603,10 +607,10 @@ describe('useId', () => {
603607
id="container"
604608
>
605609
<div>
606-
custom-prefix-R:1
610+
:custom-prefix-R1:
607611
</div>
608612
<div>
609-
custom-prefix-R:2
613+
:custom-prefix-R2:
610614
</div>
611615
</div>
612616
`);
@@ -620,13 +624,13 @@ describe('useId', () => {
620624
id="container"
621625
>
622626
<div>
623-
custom-prefix-R:1
627+
:custom-prefix-R1:
624628
</div>
625629
<div>
626-
custom-prefix-R:2
630+
:custom-prefix-R2:
627631
</div>
628632
<div>
629-
custom-prefix-r:0
633+
:custom-prefix-r0:
630634
</div>
631635
</div>
632636
`);

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export function createResponseState(
130130
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
131131
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
132132
boundaryPrefix: idPrefix + 'B:',
133-
idPrefix: idPrefix + 'R:',
133+
idPrefix: idPrefix,
134134
nextSuspenseID: 0,
135135
sentCompleteSegmentFunction: false,
136136
sentCompleteBoundaryFunction: false,
@@ -242,13 +242,13 @@ export function makeId(
242242
): string {
243243
const idPrefix = responseState.idPrefix;
244244

245-
let id = idPrefix + treeId;
245+
let id = ':' + idPrefix + 'R' + treeId + ':';
246246

247247
// Unless this is the first id at this level, append a number at the end
248248
// that represents the position of this useId hook among all the useId
249249
// hooks for this fiber.
250250
if (localId > 0) {
251-
id += ':' + localId.toString(32);
251+
id += localId.toString(32) + ':';
252252
}
253253

254254
return id;

packages/react-reconciler/src/ReactFiberHooks.new.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -2072,19 +2072,19 @@ function mountId(): string {
20722072
const treeId = getTreeId();
20732073

20742074
// Use a captial R prefix for server-generated ids.
2075-
id = identifierPrefix + 'R:' + treeId;
2075+
id = ':' + identifierPrefix + 'R' + treeId + ':';
20762076

20772077
// Unless this is the first id at this level, append a number at the end
20782078
// that represents the position of this useId hook among all the useId
20792079
// hooks for this fiber.
20802080
const localId = localIdCounter++;
20812081
if (localId > 0) {
2082-
id += ':' + localId.toString(32);
2082+
id += localId.toString(32) + ':';
20832083
}
20842084
} else {
20852085
// Use a lowercase r prefix for client-generated ids.
20862086
const globalClientId = globalClientIdCounter++;
2087-
id = identifierPrefix + 'r:' + globalClientId.toString(32);
2087+
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
20882088
}
20892089

20902090
hook.memoizedState = id;

packages/react-reconciler/src/ReactFiberHooks.old.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -2072,19 +2072,19 @@ function mountId(): string {
20722072
const treeId = getTreeId();
20732073

20742074
// Use a captial R prefix for server-generated ids.
2075-
id = identifierPrefix + 'R:' + treeId;
2075+
id = ':' + identifierPrefix + 'R' + treeId + ':';
20762076

20772077
// Unless this is the first id at this level, append a number at the end
20782078
// that represents the position of this useId hook among all the useId
20792079
// hooks for this fiber.
20802080
const localId = localIdCounter++;
20812081
if (localId > 0) {
2082-
id += ':' + localId.toString(32);
2082+
id += localId.toString(32) + ':';
20832083
}
20842084
} else {
20852085
// Use a lowercase r prefix for client-generated ids.
20862086
const globalClientId = globalClientIdCounter++;
2087-
id = identifierPrefix + 'r:' + globalClientId.toString(32);
2087+
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
20882088
}
20892089

20902090
hook.memoizedState = id;

0 commit comments

Comments
 (0)