From efe4121ee218099539523a713272edadbaafca2a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 24 Feb 2022 20:07:41 -0500 Subject: [PATCH] Add : to beginning and end of every useId (#23360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../ReactHooksInspectionIntegration-test.js | 2 +- .../src/__tests__/ReactDOMUseId-test.js | 26 +++++++++++-------- .../src/server/ReactDOMServerFormatConfig.js | 6 ++--- .../src/ReactFiberHooks.new.js | 6 ++--- .../src/ReactFiberHooks.old.js | 6 ++--- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 5b36039ecc5dc..f5003e96bdf6c 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -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, diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index 036c138d5e97a..41c4ddcbcc489 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -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 ( @@ -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 @@ -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?))*:$/); } }); @@ -338,7 +342,7 @@ describe('useId', () => {
- R:0, R:0:1, R:0:2 + :R0:, :R0:1:, :R0:2:
`); @@ -364,7 +368,7 @@ describe('useId', () => {
- R:0 + :R0:
`); @@ -603,10 +607,10 @@ describe('useId', () => { id="container" >
- custom-prefix-R:1 + :custom-prefix-R1:
- custom-prefix-R:2 + :custom-prefix-R2:
`); @@ -620,13 +624,13 @@ describe('useId', () => { id="container" >
- custom-prefix-R:1 + :custom-prefix-R1:
- custom-prefix-R:2 + :custom-prefix-R2:
- custom-prefix-r:0 + :custom-prefix-r0:
`); diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 20887c226015a..f623556bb62de 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -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, @@ -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; diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 3f64a7d8f9178..5c4b9357f909e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -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; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index d55cac9b26c74..f0fcef733d581 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -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;