Skip to content

Commit 1441c66

Browse files
authored
Fix @reach/auto-id issue by removing the dependency (#1484)
1 parent 054a5fc commit 1441c66

5 files changed

Lines changed: 171 additions & 33 deletions

File tree

packages/react-day-picker/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@
3232
"src",
3333
"tsconfig.json"
3434
],
35-
"dependencies": {
36-
"@reach/auto-id": "0.16.0"
37-
},
3835
"devDependencies": {
3936
"@rollup/plugin-alias": "^3.1.9",
4037
"@rollup/plugin-commonjs": "^21.1.0",

packages/react-day-picker/src/components/Month/Month.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import React from 'react';
22

3-
import { useId } from '@reach/auto-id';
4-
53
import { Caption } from 'components/Caption';
64
import { Table } from 'components/Table';
75
import { useDayPicker } from 'contexts/DayPicker';
86
import { useNavigation } from 'contexts/Navigation';
7+
import { useId } from 'hooks/useId';
98

109
/** The props for the [[Month]] component. */
1110
export interface MonthProps {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useId';
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2018-present, React Training LLC
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7+
8+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9+
10+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11+
*/
12+
13+
/* eslint-disable prefer-const */
14+
/* eslint-disable @typescript-eslint/ban-ts-comment */
15+
/*
16+
* Welcome to @reach/auto-id!
17+
* Let's see if we can make sense of why this hook exists and its
18+
* implementation.
19+
*
20+
* Some background:
21+
* 1. Accessibility APIs rely heavily on element IDs
22+
* 2. Requiring developers to put IDs on every element in Reach UI is both
23+
* cumbersome and error-prone
24+
* 3. With a component model, we can generate IDs for them!
25+
*
26+
* Solution 1: Generate random IDs.
27+
*
28+
* This works great as long as you don't server render your app. When React (in
29+
* the client) tries to reuse the markup from the server, the IDs won't match
30+
* and React will then recreate the entire DOM tree.
31+
*
32+
* Solution 2: Increment an integer
33+
*
34+
* This sounds great. Since we're rendering the exact same tree on the server
35+
* and client, we can increment a counter and get a deterministic result between
36+
* client and server. Also, JS integers can go up to nine-quadrillion. I'm
37+
* pretty sure the tab will be closed before an app never needs
38+
* 10 quadrillion IDs!
39+
*
40+
* Problem solved, right?
41+
*
42+
* Ah, but there's a catch! React's concurrent rendering makes this approach
43+
* non-deterministic. While the client and server will end up with the same
44+
* elements in the end, depending on suspense boundaries (and possibly some user
45+
* input during the initial render) the incrementing integers won't always match
46+
* up.
47+
*
48+
* Solution 3: Don't use IDs at all on the server; patch after first render.
49+
*
50+
* What we've done here is solution 2 with some tricks. With this approach, the
51+
* ID returned is an empty string on the first render. This way the server and
52+
* client have the same markup no matter how wild the concurrent rendering may
53+
* have gotten.
54+
*
55+
* After the render, we patch up the components with an incremented ID. This
56+
* causes a double render on any components with `useId`. Shouldn't be a problem
57+
* since the components using this hook should be small, and we're only updating
58+
* the ID attribute on the DOM, nothing big is happening.
59+
*
60+
* It doesn't have to be an incremented number, though--we could do generate
61+
* random strings instead, but incrementing a number is probably the cheapest
62+
* thing we can do.
63+
*
64+
* Additionally, we only do this patchup on the very first client render ever.
65+
* Any calls to `useId` that happen dynamically in the client will be
66+
* populated immediately with a value. So, we only get the double render after
67+
* server hydration and never again, SO BACK OFF ALRIGHT?
68+
*/
69+
70+
import * as React from 'react';
71+
72+
function canUseDOM() {
73+
return !!(
74+
typeof window !== 'undefined' &&
75+
window.document &&
76+
window.document.createElement
77+
);
78+
}
79+
/**
80+
* React currently throws a warning when using useLayoutEffect on the server. To
81+
* get around it, we can conditionally useEffect on the server (no-op) and
82+
* useLayoutEffect in the browser. We occasionally need useLayoutEffect to
83+
* ensure we don't get a render flash for certain operations, but we may also
84+
* need affected components to render on the server. One example is when setting
85+
* a component's descendants to retrieve their index values.
86+
*
87+
* Important to note that using this hook as an escape hatch will break the
88+
* eslint dependency warnings unless you rename the import to `useLayoutEffect`.
89+
* Use sparingly only when the effect won't effect the rendered HTML to avoid
90+
* any server/client mismatch.
91+
*
92+
* If a useLayoutEffect is needed and the result would create a mismatch, it's
93+
* likely that the component in question shouldn't be rendered on the server at
94+
* all, so a better approach would be to lazily render those in a parent
95+
* component after client-side hydration.
96+
*
97+
* https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
98+
* https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.js
99+
*
100+
* @param effect
101+
* @param deps
102+
*/
103+
const useIsomorphicLayoutEffect = canUseDOM()
104+
? React.useLayoutEffect
105+
: React.useEffect;
106+
107+
let serverHandoffComplete = false;
108+
let id = 0;
109+
function genId() {
110+
return ++id;
111+
}
112+
113+
/* eslint-disable react-hooks/rules-of-hooks */
114+
115+
/**
116+
* useId
117+
*
118+
* Autogenerate IDs to facilitate WAI-ARIA and server rendering.
119+
*
120+
* Note: The returned ID will initially be `null` and will update after a
121+
* component mounts. Users may need to supply their own ID if they need
122+
* consistent values for SSR.
123+
*
124+
* @see Docs https://reach.tech/auto-id
125+
*/
126+
function useId(idFromProps: string): string;
127+
function useId(idFromProps: number): number;
128+
function useId(idFromProps: string | number): string | number;
129+
function useId(idFromProps: string | undefined | null): string | undefined;
130+
function useId(idFromProps: number | undefined | null): number | undefined;
131+
function useId(
132+
idFromProps: string | number | undefined | null
133+
): string | number | undefined;
134+
function useId(): string | undefined;
135+
136+
function useId(providedId?: number | string | undefined | null) {
137+
// TODO: Remove error flag when updating internal deps to React 18. None of
138+
// our tricks will play well with concurrent rendering anyway.
139+
140+
// If this instance isn't part of the initial render, we don't have to do the
141+
// double render/patch-up dance. We can just generate the ID and return it.
142+
let initialId = providedId ?? (serverHandoffComplete ? genId() : null);
143+
let [id, setId] = React.useState(initialId);
144+
145+
useIsomorphicLayoutEffect(() => {
146+
if (id === null) {
147+
// Patch the ID after render. We do this in `useLayoutEffect` to avoid any
148+
// rendering flicker, though it'll make the first render slower (unlikely
149+
// to matter, but you're welcome to measure your app and let us know if
150+
// it's a problem).
151+
setId(genId());
152+
}
153+
// eslint-disable-next-line react-hooks/exhaustive-deps
154+
}, []);
155+
156+
React.useEffect(() => {
157+
if (serverHandoffComplete === false) {
158+
// Flag all future uses of `useId` to skip the update dance. This is in
159+
// `useEffect` because it goes after `useLayoutEffect`, ensuring we don't
160+
// accidentally bail out of the patch-up dance prematurely.
161+
serverHandoffComplete = true;
162+
}
163+
}, []);
164+
165+
return providedId ?? id ?? undefined;
166+
}
167+
168+
export { useId };

yarn.lock

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3902,32 +3902,6 @@ __metadata:
39023902
languageName: node
39033903
linkType: hard
39043904

3905-
"@reach/auto-id@npm:0.16.0":
3906-
version: 0.16.0
3907-
resolution: "@reach/auto-id@npm:0.16.0"
3908-
dependencies:
3909-
"@reach/utils": 0.16.0
3910-
tslib: ^2.3.0
3911-
peerDependencies:
3912-
react: ^16.8.0 || 17.x
3913-
react-dom: ^16.8.0 || 17.x
3914-
checksum: 80211f7db1c0e3b107e2c1d3fe8055e7e41d7399b51ff0da5f0c9df43d90b77aa4a31a896545c699f9629b3fbc3c762ee0518de70faf8112098886c6759d3f15
3915-
languageName: node
3916-
linkType: hard
3917-
3918-
"@reach/utils@npm:0.16.0":
3919-
version: 0.16.0
3920-
resolution: "@reach/utils@npm:0.16.0"
3921-
dependencies:
3922-
tiny-warning: ^1.0.3
3923-
tslib: ^2.3.0
3924-
peerDependencies:
3925-
react: ^16.8.0 || 17.x
3926-
react-dom: ^16.8.0 || 17.x
3927-
checksum: 36bc0eb41a71798eb6186b23de265ba709e51dae5bf214fb8505c66bb3f2e6a41bb2401457350436ba89ca9e3a50f93a04fe7c33d15648ce11e568a85622d770
3928-
languageName: node
3929-
linkType: hard
3930-
39313905
"@react-day-picker/monorepo@workspace:.":
39323906
version: 0.0.0-use.local
39333907
resolution: "@react-day-picker/monorepo@workspace:."
@@ -13691,7 +13665,6 @@ __metadata:
1369113665
version: 0.0.0-use.local
1369213666
resolution: "react-day-picker@workspace:packages/react-day-picker"
1369313667
dependencies:
13694-
"@reach/auto-id": 0.16.0
1369513668
"@rollup/plugin-alias": ^3.1.9
1369613669
"@rollup/plugin-commonjs": ^21.1.0
1369713670
"@rollup/plugin-typescript": ^8.3.1
@@ -15760,7 +15733,7 @@ __metadata:
1576015733
languageName: node
1576115734
linkType: hard
1576215735

15763-
"tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1":
15736+
"tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1":
1576415737
version: 2.3.1
1576515738
resolution: "tslib@npm:2.3.1"
1576615739
checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9

0 commit comments

Comments
 (0)