Skip to content

Commit

Permalink
Add : to beginning and end of every useId (#23360)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite authored Feb 25, 2022
1 parent 42f15b3 commit efe4121
Show file tree
Hide file tree
Showing 5 changed files with 25 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ describe('ReactHooksInspectionIntegration', () => {
expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('Id');
expect(String(tree[0].value).startsWith('r:')).toBe(true);
expect(String(tree[0].value).startsWith(':r')).toBe(true);

expect(tree[1]).toEqual({
id: 1,
Expand Down
26 changes: 15 additions & 11 deletions packages/react-dom/src/__tests__/ReactDOMUseId-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ describe('useId', () => {
}

function normalizeTreeIdForTesting(id) {
const [serverClientPrefix, base32, hookIndex] = id.split(':');
const result = id.match(/:(R|r)(.*):(([0-9]*):)?/);
if (result === undefined) {
throw new Error('Invalid id format');
}
const [, serverClientPrefix, base32, hookIndex] = result;
if (serverClientPrefix.endsWith('r')) {
// Client ids aren't stable. For testing purposes, strip out the counter.
return (
Expand Down Expand Up @@ -278,7 +282,7 @@ describe('useId', () => {
// 'R:' prefix, and the first character after that, which may not correspond
// to a complete set of 5 bits.
//
// Example: R:clalalalalalalala...
// Example: :Rclalalalalalalala...:
//
// We can use this pattern to test large ids that exceed the bitwise
// safe range (32 bits). The algorithm should theoretically support ids
Expand Down Expand Up @@ -313,8 +317,8 @@ describe('useId', () => {

// Confirm that every id matches the expected pattern
for (let i = 0; i < divs.length; i++) {
// Example: R:clalalalalalalala...
expect(divs[i].id).toMatch(/^R:.(((al)*a?)((la)*l?))*$/);
// Example: :Rclalalalalalalala...:
expect(divs[i].id).toMatch(/^:R.(((al)*a?)((la)*l?))*:$/);
}
});

Expand All @@ -338,7 +342,7 @@ describe('useId', () => {
<div
id="container"
>
R:0, R:0:1, R:0:2
:R0:, :R0:1:, :R0:2:
<!-- -->
</div>
`);
Expand All @@ -364,7 +368,7 @@ describe('useId', () => {
<div
id="container"
>
R:0
:R0:
<!-- -->
</div>
`);
Expand Down Expand Up @@ -603,10 +607,10 @@ describe('useId', () => {
id="container"
>
<div>
custom-prefix-R:1
:custom-prefix-R1:
</div>
<div>
custom-prefix-R:2
:custom-prefix-R2:
</div>
</div>
`);
Expand All @@ -620,13 +624,13 @@ describe('useId', () => {
id="container"
>
<div>
custom-prefix-R:1
:custom-prefix-R1:
</div>
<div>
custom-prefix-R:2
:custom-prefix-R2:
</div>
<div>
custom-prefix-r:0
:custom-prefix-r0:
</div>
</div>
`);
Expand Down
6 changes: 3 additions & 3 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export function createResponseState(
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
boundaryPrefix: idPrefix + 'B:',
idPrefix: idPrefix + 'R:',
idPrefix: idPrefix,
nextSuspenseID: 0,
sentCompleteSegmentFunction: false,
sentCompleteBoundaryFunction: false,
Expand Down Expand Up @@ -242,13 +242,13 @@ export function makeId(
): string {
const idPrefix = responseState.idPrefix;

let id = idPrefix + treeId;
let id = ':' + idPrefix + 'R' + treeId + ':';

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

return id;
Expand Down
6 changes: 3 additions & 3 deletions packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -2072,19 +2072,19 @@ function mountId(): string {
const treeId = getTreeId();

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

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

hook.memoizedState = id;
Expand Down
6 changes: 3 additions & 3 deletions packages/react-reconciler/src/ReactFiberHooks.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -2072,19 +2072,19 @@ function mountId(): string {
const treeId = getTreeId();

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

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

hook.memoizedState = id;
Expand Down

0 comments on commit efe4121

Please sign in to comment.