Repository: facebook/react
Pull request: Use valid CSS selectors in useId format

For the `useId` algorithm we used colon `:` before and after. https://github.com/facebook/react/pull/23360

This avoids collisions in general by using an unusual characters. It also avoids collisions when concatenated with some other ID. Unfortunately, `:` is not a valid character in `view-transition-name`.

This PR swaps the format from:

```
:r123:
```

To the unicode:

```
«r123»
```

Which is valid CSS selectors. This also allows them being used for `querySelector()` which we didn't really find a legit use for but seems ok-ish.

That way you can get a view-transition-name that you can manually reference. E.g. to generate styles:

```js
const id = useId();
return <>
  <style>{`
    ::view-transition-group(${id}) { ... }
    ::view-transition-old(${id}) { ... }
    ::view-transition-new(${id}) { ... }
  `}</style>
  <ViewTransition name={id}>...</ViewTransition>
</>;
```

Changed files:
filename: packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
patch: @@ -1553,7 +1553,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('\u00ABr')).toBe(true);
 
     expect(normalizeSourceLoc(tree)[1]).toMatchInlineSnapshot(`
       {

filename: packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
patch: @@ -858,7 +858,7 @@ export function makeId(
 ): string {
   const idPrefix = resumableState.idPrefix;
 
-  let id = ':' + idPrefix + 'R' + treeId;
+  let id = '\u00AB' + 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
@@ -867,7 +867,7 @@ export function makeId(
     id += 'H' + localId.toString(32);
   }
 
-  return id + ':';
+  return id + '\u00BB';
 }
 
 function encodeHTMLTextNode(text: string): string {

filename: packages/react-dom/src/__tests__/ReactDOMUseId-test.js
patch: @@ -96,7 +96,7 @@ describe('useId', () => {
   }
 
   function normalizeTreeIdForTesting(id) {
-    const result = id.match(/:(R|r)([a-z0-9]*)(H([0-9]*))?:/);
+    const result = id.match(/\u00AB(R|r)([a-z0-9]*)(H([0-9]*))?\u00BB/);
     if (result === undefined) {
       throw new Error('Invalid id format');
     }
@@ -285,7 +285,7 @@ describe('useId', () => {
     // 'R:' prefix, and the first character after that, which may not correspond
     // to a complete set of 5 bits.
     //
-    // Example: :Rclalalalalalalala...:
+    // 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
@@ -320,8 +320,8 @@ describe('useId', () => {
 
     // Confirm that every id matches the expected pattern
     for (let i = 0; i < divs.length; i++) {
-      // Example: :Rclalalalalalalala...:
-      expect(divs[i].id).toMatch(/^:R.(((al)*a?)((la)*l?))*:$/);
+      // Example: «Rclalalalalalalala...:
+      expect(divs[i].id).toMatch(/^\u00ABR.(((al)*a?)((la)*l?))*\u00BB$/);
     }
   });
 
@@ -345,7 +345,7 @@ describe('useId', () => {
       <div
         id="container"
       >
-        :R0:, :R0H1:, :R0H2:
+        «R0», «R0H1», «R0H2»
       </div>
     `);
   });
@@ -370,7 +370,7 @@ describe('useId', () => {
       <div
         id="container"
       >
-        :R0:
+        «R0»
       </div>
     `);
   });
@@ -608,10 +608,10 @@ describe('useId', () => {
         id="container"
       >
         <div>
-          :custom-prefix-R1:
+          «custom-prefix-R1»
         </div>
         <div>
-          :custom-prefix-R2:
+          «custom-prefix-R2»
         </div>
       </div>
     `);
@@ -625,13 +625,13 @@ describe('useId', () => {
         id="container"
       >
         <div>
-          :custom-prefix-R1:
+          «custom-prefix-R1»
         </div>
         <div>
-          :custom-prefix-R2:
+          «custom-prefix-R2»
         </div>
         <div>
-          :custom-prefix-r0:
+          «custom-prefix-r0»
         </div>
       </div>
     `);
@@ -672,11 +672,11 @@ describe('useId', () => {
         id="container"
       >
         <div>
-          :R0:
+          «R0»
           <!-- -->
            
           <div>
-            :R7:
+            «R7»
           </div>
         </div>
       </div>
@@ -690,11 +690,11 @@ describe('useId', () => {
         id="container"
       >
         <div>
-          :R0:
+          «R0»
           <!-- -->
            
           <div>
-            :R7:
+            «R7»
           </div>
         </div>
       </div>

filename: packages/react-reconciler/src/ReactFiberHooks.js
patch: @@ -3595,7 +3595,7 @@ function mountId(): string {
     const treeId = getTreeId();
 
     // Use a captial R prefix for server-generated ids.
-    id = ':' + identifierPrefix + 'R' + treeId;
+    id = '\u00AB' + 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
@@ -3605,11 +3605,16 @@ function mountId(): string {
       id += 'H' + localId.toString(32);
     }
 
-    id += ':';
+    id += '\u00BB';
   } else {
     // Use a lowercase r prefix for client-generated ids.
     const globalClientId = globalClientIdCounter++;
-    id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
+    id =
+      '\u00AB' +
+      identifierPrefix +
+      'r' +
+      globalClientId.toString(32) +
+      '\u00BB';
   }
 
   hook.memoizedState = id;