-
Notifications
You must be signed in to change notification settings - Fork 559
/
Copy pathreach-auto-id.ts
129 lines (117 loc) · 4.84 KB
/
reach-auto-id.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/*
* Welcome to @reach/auto-id!
* Let's see if we can make sense of why this hook exists and its
* implementation.
*
* Some background:
* 1. Accessibility APIs rely heavily on element IDs
* 2. Requiring developers to put IDs on every element in Reach UI is both
* cumbersome and error-prone
* 3. With a component model, we can generate IDs for them!
*
* Solution 1: Generate random IDs.
*
* This works great as long as you don't server render your app. When React (in
* the client) tries to reuse the markup from the server, the IDs won't match
* and React will then recreate the entire DOM tree.
*
* Solution 2: Increment an integer
*
* This sounds great. Since we're rendering the exact same tree on the server
* and client, we can increment a counter and get a deterministic result between
* client and server. Also, JS integers can go up to nine-quadrillion. I'm
* pretty sure the tab will be closed before an app never needs
* 10 quadrillion IDs!
*
* Problem solved, right?
*
* Ah, but there's a catch! React's concurrent rendering makes this approach
* non-deterministic. While the client and server will end up with the same
* elements in the end, depending on suspense boundaries (and possibly some user
* input during the initial render) the incrementing integers won't always match
* up.
*
* Solution 3: Don't use IDs at all on the server; patch after first render.
*
* What we've done here is solution 2 with some tricks. With this approach, the
* ID returned is an empty string on the first render. This way the server and
* client have the same markup no matter how wild the concurrent rendering may
* have gotten.
*
* After the render, we patch up the components with an incremented ID. This
* causes a double render on any components with `useId`. Shouldn't be a problem
* since the components using this hook should be small, and we're only updating
* the ID attribute on the DOM, nothing big is happening.
*
* It doesn't have to be an incremented number, though--we could do generate
* random strings instead, but incrementing a number is probably the cheapest
* thing we can do.
*
* Additionally, we only do this patchup on the very first client render ever.
* Any calls to `useId` that happen dynamically in the client will be
* populated immediately with a value. So, we only get the double render after
* server hydration and never again, SO BACK OFF ALRIGHT?
*/
/* eslint-disable react-hooks/rules-of-hooks */
import * as React from "react";
import { useIsomorphicLayoutEffect as useLayoutEffect } from "@reach/utils";
let serverHandoffComplete = false;
let id = 0;
function genId() {
return ++id;
}
// Workaround for https://github.com/webpack/webpack/issues/14814
// https://github.com/eps1lon/material-ui/blob/8d5f135b4d7a58253a99ab56dce4ac8de61f5dc1/packages/mui-utils/src/useId.ts#L21
const maybeReactUseId: undefined | (() => string) = (React as any)[
"useId".toString()
];
/**
* useId
*
* Autogenerate IDs to facilitate WAI-ARIA and server rendering.
*
* Note: The returned ID will initially be `null` and will update after a
* component mounts. Users may need to supply their own ID if they need
* consistent values for SSR.
*
* @see Docs https://reach.tech/auto-id
*/
function useId(idFromProps: string): string;
function useId(idFromProps: number): number;
function useId(idFromProps: string | number): string | number;
function useId(idFromProps: string | undefined | null): string | undefined;
function useId(idFromProps: number | undefined | null): number | undefined;
function useId(
idFromProps: string | number | undefined | null
): string | number | undefined;
function useId(): string | undefined;
function useId(providedId?: number | string | undefined | null) {
if (maybeReactUseId !== undefined) {
let generatedId = maybeReactUseId();
return providedId ?? generatedId;
}
// If this instance isn't part of the initial render, we don't have to do the
// double render/patch-up dance. We can just generate the ID and return it.
let initialId = providedId ?? (serverHandoffComplete ? genId() : null);
let [id, setId] = React.useState(initialId);
useLayoutEffect(() => {
if (id === null) {
// Patch the ID after render. We do this in `useLayoutEffect` to avoid any
// rendering flicker, though it'll make the first render slower (unlikely
// to matter, but you're welcome to measure your app and let us know if
// it's a problem).
setId(genId());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (serverHandoffComplete === false) {
// Flag all future uses of `useId` to skip the update dance. This is in
// `useEffect` because it goes after `useLayoutEffect`, ensuring we don't
// accidentally bail out of the patch-up dance prematurely.
serverHandoffComplete = true;
}
}, []);
return providedId ?? id ?? undefined;
}
export { useId };