Skip to content

Commit 2e4db33

Browse files
authored
Use valid CSS selectors in useId format (#32001)
For the `useId` algorithm we used colon `:` before and after. #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> </>; ```
1 parent d42a90c commit 2e4db33

File tree

4 files changed

+26
-21
lines changed

4 files changed

+26
-21
lines changed

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1553,7 +1553,7 @@ describe('ReactHooksInspectionIntegration', () => {
15531553
expect(tree[0].id).toEqual(0);
15541554
expect(tree[0].isStateEditable).toEqual(false);
15551555
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);
15571557

15581558
expect(normalizeSourceLoc(tree)[1]).toMatchInlineSnapshot(`
15591559
{

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,7 @@ export function makeId(
858858
): string {
859859
const idPrefix = resumableState.idPrefix;
860860

861-
let id = ':' + idPrefix + 'R' + treeId;
861+
let id = '\u00AB' + idPrefix + 'R' + treeId;
862862

863863
// Unless this is the first id at this level, append a number at the end
864864
// that represents the position of this useId hook among all the useId
@@ -867,7 +867,7 @@ export function makeId(
867867
id += 'H' + localId.toString(32);
868868
}
869869

870-
return id + ':';
870+
return id + '\u00BB';
871871
}
872872

873873
function encodeHTMLTextNode(text: string): string {

packages/react-dom/src/__tests__/ReactDOMUseId-test.js

+15-15
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe('useId', () => {
9696
}
9797

9898
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/);
100100
if (result === undefined) {
101101
throw new Error('Invalid id format');
102102
}
@@ -285,7 +285,7 @@ describe('useId', () => {
285285
// 'R:' prefix, and the first character after that, which may not correspond
286286
// to a complete set of 5 bits.
287287
//
288-
// Example: :Rclalalalalalalala...:
288+
// Example: «Rclalalalalalalala...:
289289
//
290290
// We can use this pattern to test large ids that exceed the bitwise
291291
// safe range (32 bits). The algorithm should theoretically support ids
@@ -320,8 +320,8 @@ describe('useId', () => {
320320

321321
// Confirm that every id matches the expected pattern
322322
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$/);
325325
}
326326
});
327327

@@ -345,7 +345,7 @@ describe('useId', () => {
345345
<div
346346
id="container"
347347
>
348-
:R0:, :R0H1:, :R0H2:
348+
«R0», «R0H1», «R0H2»
349349
</div>
350350
`);
351351
});
@@ -370,7 +370,7 @@ describe('useId', () => {
370370
<div
371371
id="container"
372372
>
373-
:R0:
373+
«R0»
374374
</div>
375375
`);
376376
});
@@ -608,10 +608,10 @@ describe('useId', () => {
608608
id="container"
609609
>
610610
<div>
611-
:custom-prefix-R1:
611+
«custom-prefix-R1»
612612
</div>
613613
<div>
614-
:custom-prefix-R2:
614+
«custom-prefix-R2»
615615
</div>
616616
</div>
617617
`);
@@ -625,13 +625,13 @@ describe('useId', () => {
625625
id="container"
626626
>
627627
<div>
628-
:custom-prefix-R1:
628+
«custom-prefix-R1»
629629
</div>
630630
<div>
631-
:custom-prefix-R2:
631+
«custom-prefix-R2»
632632
</div>
633633
<div>
634-
:custom-prefix-r0:
634+
«custom-prefix-r0»
635635
</div>
636636
</div>
637637
`);
@@ -672,11 +672,11 @@ describe('useId', () => {
672672
id="container"
673673
>
674674
<div>
675-
:R0:
675+
«R0»
676676
<!-- -->
677677
678678
<div>
679-
:R7:
679+
«R7»
680680
</div>
681681
</div>
682682
</div>
@@ -690,11 +690,11 @@ describe('useId', () => {
690690
id="container"
691691
>
692692
<div>
693-
:R0:
693+
«R0»
694694
<!-- -->
695695
696696
<div>
697-
:R7:
697+
«R7»
698698
</div>
699699
</div>
700700
</div>

packages/react-reconciler/src/ReactFiberHooks.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -3595,7 +3595,7 @@ function mountId(): string {
35953595
const treeId = getTreeId();
35963596

35973597
// Use a captial R prefix for server-generated ids.
3598-
id = ':' + identifierPrefix + 'R' + treeId;
3598+
id = '\u00AB' + identifierPrefix + 'R' + treeId;
35993599

36003600
// Unless this is the first id at this level, append a number at the end
36013601
// that represents the position of this useId hook among all the useId
@@ -3605,11 +3605,16 @@ function mountId(): string {
36053605
id += 'H' + localId.toString(32);
36063606
}
36073607

3608-
id += ':';
3608+
id += '\u00BB';
36093609
} else {
36103610
// Use a lowercase r prefix for client-generated ids.
36113611
const globalClientId = globalClientIdCounter++;
3612-
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
3612+
id =
3613+
'\u00AB' +
3614+
identifierPrefix +
3615+
'r' +
3616+
globalClientId.toString(32) +
3617+
'\u00BB';
36133618
}
36143619

36153620
hook.memoizedState = id;

0 commit comments

Comments
 (0)