Repository: facebook/react
Pull request: useId: Use 'H' to separate main id from hook index

No id should be a subset of any other id. Currently, this is not true when there are multiple hooks in the same component. We append the hook index to the end of the id, except for the first one. So you get this pattern.

Before this change:

- 1st hook's id: `:R0:`
- 2nd hook's id: `:R0:1:`

The first hook's id is a subset of all the other ids in the same component.

The fix for this is to use a different character to separate the main id from the hook index. I've chosen a capital 'H' for this because capital letters are not part of the base 32 character set when encoding with `toString(32)`.

After this change:

- 1st hook's id: `:R0:`
- 2nd hook's id: `:R0H1:`

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMUseId-test.js
patch: @@ -93,7 +93,7 @@ describe('useId', () => {
   }
 
   function normalizeTreeIdForTesting(id) {
-    const result = id.match(/:(R|r)(.*):(([0-9]*):)?/);
+    const result = id.match(/:(R|r)([a-z0-9]*)(H([0-9]*))?:/);
     if (result === undefined) {
       throw new Error('Invalid id format');
     }
@@ -342,7 +342,7 @@ describe('useId', () => {
       <div
         id="container"
       >
-        :R0:, :R0:1:, :R0:2:
+        :R0:, :R0H1:, :R0H2:
         <!-- -->
       </div>
     `);

filename: packages/react-dom/src/server/ReactDOMServerFormatConfig.js
patch: @@ -242,16 +242,16 @@ export function makeId(
 ): string {
   const idPrefix = responseState.idPrefix;
 
-  let id = ':' + idPrefix + 'R' + 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 += 'H' + localId.toString(32);
   }
 
-  return id;
+  return id + ':';
 }
 
 function encodeHTMLTextNode(text: string): string {

filename: packages/react-reconciler/src/ReactFiberHooks.new.js
patch: @@ -2072,15 +2072,17 @@ 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 += 'H' + localId.toString(32);
     }
+
+    id += ':';
   } else {
     // Use a lowercase r prefix for client-generated ids.
     const globalClientId = globalClientIdCounter++;

filename: packages/react-reconciler/src/ReactFiberHooks.old.js
patch: @@ -2072,15 +2072,17 @@ 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 += 'H' + localId.toString(32);
     }
+
+    id += ':';
   } else {
     // Use a lowercase r prefix for client-generated ids.
     const globalClientId = globalClientIdCounter++;