Skip to content

Commit

Permalink
Fix @reach/auto-id issue by removing the dependency (#1484)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpbl authored Jul 18, 2022
1 parent 7e03a08 commit a513d9c
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 33 deletions.
3 changes: 0 additions & 3 deletions packages/react-day-picker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@
"src",
"tsconfig.json"
],
"dependencies": {
"@reach/auto-id": "0.16.0"
},
"devDependencies": {
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^21.1.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/react-day-picker/src/components/Month/Month.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React from 'react';

import { useId } from '@reach/auto-id';

import { Caption } from 'components/Caption';
import { Table } from 'components/Table';
import { useDayPicker } from 'contexts/DayPicker';
import { useNavigation } from 'contexts/Navigation';
import { useId } from 'hooks/useId';

/** The props for the [[Month]] component. */
export interface MonthProps {
Expand Down
1 change: 1 addition & 0 deletions packages/react-day-picker/src/hooks/useId/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useId';
168 changes: 168 additions & 0 deletions packages/react-day-picker/src/hooks/useId/useId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
The MIT License (MIT)
Copyright (c) 2018-present, React Training LLC
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
*/

/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/*
* 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?
*/

import * as React from 'react';

function canUseDOM() {
return !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
}
/**
* React currently throws a warning when using useLayoutEffect on the server. To
* get around it, we can conditionally useEffect on the server (no-op) and
* useLayoutEffect in the browser. We occasionally need useLayoutEffect to
* ensure we don't get a render flash for certain operations, but we may also
* need affected components to render on the server. One example is when setting
* a component's descendants to retrieve their index values.
*
* Important to note that using this hook as an escape hatch will break the
* eslint dependency warnings unless you rename the import to `useLayoutEffect`.
* Use sparingly only when the effect won't effect the rendered HTML to avoid
* any server/client mismatch.
*
* If a useLayoutEffect is needed and the result would create a mismatch, it's
* likely that the component in question shouldn't be rendered on the server at
* all, so a better approach would be to lazily render those in a parent
* component after client-side hydration.
*
* https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
* https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.js
*
* @param effect
* @param deps
*/
const useIsomorphicLayoutEffect = canUseDOM()
? React.useLayoutEffect
: React.useEffect;

let serverHandoffComplete = false;
let id = 0;
function genId() {
return ++id;
}

/* eslint-disable react-hooks/rules-of-hooks */

/**
* 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) {
// TODO: Remove error flag when updating internal deps to React 18. None of
// our tricks will play well with concurrent rendering anyway.

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

useIsomorphicLayoutEffect(() => {
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 };
29 changes: 1 addition & 28 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3902,32 +3902,6 @@ __metadata:
languageName: node
linkType: hard

"@reach/auto-id@npm:0.16.0":
version: 0.16.0
resolution: "@reach/auto-id@npm:0.16.0"
dependencies:
"@reach/utils": 0.16.0
tslib: ^2.3.0
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
checksum: 80211f7db1c0e3b107e2c1d3fe8055e7e41d7399b51ff0da5f0c9df43d90b77aa4a31a896545c699f9629b3fbc3c762ee0518de70faf8112098886c6759d3f15
languageName: node
linkType: hard

"@reach/utils@npm:0.16.0":
version: 0.16.0
resolution: "@reach/utils@npm:0.16.0"
dependencies:
tiny-warning: ^1.0.3
tslib: ^2.3.0
peerDependencies:
react: ^16.8.0 || 17.x
react-dom: ^16.8.0 || 17.x
checksum: 36bc0eb41a71798eb6186b23de265ba709e51dae5bf214fb8505c66bb3f2e6a41bb2401457350436ba89ca9e3a50f93a04fe7c33d15648ce11e568a85622d770
languageName: node
linkType: hard

"@react-day-picker/monorepo@workspace:.":
version: 0.0.0-use.local
resolution: "@react-day-picker/monorepo@workspace:."
Expand Down Expand Up @@ -13691,7 +13665,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "react-day-picker@workspace:packages/react-day-picker"
dependencies:
"@reach/auto-id": 0.16.0
"@rollup/plugin-alias": ^3.1.9
"@rollup/plugin-commonjs": ^21.1.0
"@rollup/plugin-typescript": ^8.3.1
Expand Down Expand Up @@ -15760,7 +15733,7 @@ __metadata:
languageName: node
linkType: hard

"tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1":
"tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.1":
version: 2.3.1
resolution: "tslib@npm:2.3.1"
checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9
Expand Down

0 comments on commit a513d9c

Please sign in to comment.