Repository: facebook/react
Pull request: Add : to beginning and end of every useId

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.

Changed files:
filename: packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
patch: @@ -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,

filename: packages/react-dom/src/__tests__/ReactDOMUseId-test.js
patch: @@ -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', () => {
       <div
         id="container"
       >
-        R:0, R:0:1, R:0:2
+        :R0:, :R0:1:, :R0:2:
         <!-- -->
       </div>
     `);
@@ -364,7 +368,7 @@ describe('useId', () => {
       <div
         id="container"
       >
-        R:0
+        :R0:
         <!-- -->
       </div>
     `);
@@ -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>
     `);
@@ -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>
     `);

filename: packages/react-dom/src/server/ReactDOMServerFormatConfig.js
patch: @@ -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;

filename: packages/react-reconciler/src/ReactFiberHooks.new.js
patch: @@ -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;

filename: packages/react-reconciler/src/ReactFiberHooks.old.js
patch: @@ -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;