@samitouri / QOS-React / commits / 2e4db3344f

Use valid CSS selectors in useId format (#32001)

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> </>; ```

Sebastian Markbåge committed Feb 25, 2025 at 12:45 UTC 2e4db3344f030fe622152ecc231a7c99a81a9c9d
4 files changed +26 -21
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+1 -1
@@ -1553,7 +1553,7 @@ describe('ReactHooksInspectionIntegration', () => {
1553 expect(tree[0].id).toEqual(0);
1554 expect(tree[0].isStateEditable).toEqual(false);
1555 expect(tree[0].name).toEqual('Id');
1556 - expect(String(tree[0].value).startsWith(':r')).toBe(true);
1556 + expect(String(tree[0].value).startsWith('\u00ABr')).toBe(true);
1557
1558 expect(normalizeSourceLoc(tree)[1]).toMatchInlineSnapshot(`
1559 {
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+2 -2
@@ -858,7 +858,7 @@ export function makeId(
858 ): string {
859 const idPrefix = resumableState.idPrefix;
860
861 - let id = ':' + idPrefix + 'R' + treeId;
861 + let id = '\u00AB' + idPrefix + 'R' + treeId;
862
863 // Unless this is the first id at this level, append a number at the end
864 // that represents the position of this useId hook among all the useId
@@ -867,7 +867,7 @@ export function makeId(
867 id += 'H' + localId.toString(32);
868 }
869
870 - return id + ':';
870 + return id + '\u00BB';
871 }
872
873 function encodeHTMLTextNode(text: string): string {
packages/react-dom/src/__tests__/ReactDOMUseId-test.js
+15 -15
@@ -96,7 +96,7 @@ describe('useId', () => {
96 }
97
98 function normalizeTreeIdForTesting(id) {
99 - const result = id.match(/:(R|r)([a-z0-9]*)(H([0-9]*))?:/);
99 + const result = id.match(/\u00AB(R|r)([a-z0-9]*)(H([0-9]*))?\u00BB/);
100 if (result === undefined) {
101 throw new Error('Invalid id format');
102 }
@@ -285,7 +285,7 @@ describe('useId', () => {
285 // 'R:' prefix, and the first character after that, which may not correspond
286 // to a complete set of 5 bits.
287 //
288 - // Example: :Rclalalalalalalala...:
288 + // Example: «Rclalalalalalalala...:
289 //
290 // We can use this pattern to test large ids that exceed the bitwise
291 // safe range (32 bits). The algorithm should theoretically support ids
@@ -320,8 +320,8 @@ describe('useId', () => {
320
321 // Confirm that every id matches the expected pattern
322 for (let i = 0; i < divs.length; i++) {
323 - // Example: :Rclalalalalalalala...:
324 - expect(divs[i].id).toMatch(/^:R.(((al)*a?)((la)*l?))*:$/);
323 + // Example: «Rclalalalalalalala...:
324 + expect(divs[i].id).toMatch(/^\u00ABR.(((al)*a?)((la)*l?))*\u00BB$/);
325 }
326 });
327
@@ -345,7 +345,7 @@ describe('useId', () => {
345 <div
346 id="container"
347 >
348 - :R0:, :R0H1:, :R0H2:
348 + «R0», «R0H1», «R0H2»
349 </div>
350 `);
351 });
@@ -370,7 +370,7 @@ describe('useId', () => {
370 <div
371 id="container"
372 >
373 - :R0:
373 + «R0»
374 </div>
375 `);
376 });
@@ -608,10 +608,10 @@ describe('useId', () => {
608 id="container"
609 >
610 <div>
611 - :custom-prefix-R1:
611 + «custom-prefix-R1»
612 </div>
613 <div>
614 - :custom-prefix-R2:
614 + «custom-prefix-R2»
615 </div>
616 </div>
617 `);
@@ -625,13 +625,13 @@ describe('useId', () => {
625 id="container"
626 >
627 <div>
628 - :custom-prefix-R1:
628 + «custom-prefix-R1»
629 </div>
630 <div>
631 - :custom-prefix-R2:
631 + «custom-prefix-R2»
632 </div>
633 <div>
634 - :custom-prefix-r0:
634 + «custom-prefix-r0»
635 </div>
636 </div>
637 `);
@@ -672,11 +672,11 @@ describe('useId', () => {
672 id="container"
673 >
674 <div>
675 - :R0:
675 + «R0»
676 <!-- -->
677
678 <div>
679 - :R7:
679 + «R7»
680 </div>
681 </div>
682 </div>
@@ -690,11 +690,11 @@ describe('useId', () => {
690 id="container"
691 >
692 <div>
693 - :R0:
693 + «R0»
694 <!-- -->
695
696 <div>
697 - :R7:
697 + «R7»
698 </div>
699 </div>
700 </div>
packages/react-reconciler/src/ReactFiberHooks.js
+8 -3
@@ -3595,7 +3595,7 @@ function mountId(): string {
3595 const treeId = getTreeId();
3596
3597 // Use a captial R prefix for server-generated ids.
3598 - id = ':' + identifierPrefix + 'R' + treeId;
3598 + id = '\u00AB' + identifierPrefix + 'R' + treeId;
3599
3600 // Unless this is the first id at this level, append a number at the end
3601 // that represents the position of this useId hook among all the useId
@@ -3605,11 +3605,16 @@ function mountId(): string {
3605 id += 'H' + localId.toString(32);
3606 }
3607
3608 - id += ':';
3608 + id += '\u00BB';
3609 } else {
3610 // Use a lowercase r prefix for client-generated ids.
3611 const globalClientId = globalClientIdCounter++;
3612 - id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
3612 + id =
3613 + '\u00AB' +
3614 + identifierPrefix +
3615 + 'r' +
3616 + globalClientId.toString(32) +
3617 + '\u00BB';
3618 }
3619
3620 hook.memoizedState = id;