diff --git a/package.json b/package.json index 7e1e242e3..d948661fd 100644 --- a/package.json +++ b/package.json @@ -162,7 +162,6 @@ "mapbox-gl": "^2.9.2", "prop-types": "^15.8.1", "react-collapsed": "^4.1.2", - "react-id-generator": "^3.0.2", "recent-searches": "^1.0.5", "tailwind-merge": "^1.3.0", "use-isomorphic-layout-effect": "^1.1.2" diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 5e9ccd8ce..650da0f83 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -17,7 +17,7 @@ import { ScreenReader } from '../ScreenReader'; import { recursivelyMapChildren } from '../utils/recursivelyMapChildren'; import { DropdownItem, DropdownItemProps, DropdownItemWithIndex } from './DropdownItem'; import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; -import { useId } from "react-id-generator"; +import { useId } from '../../hooks/useId'; const useRootClose = typeof useRootClosePkg === 'function' ? useRootClosePkg : useRootClosePkg['default']; const useLayoutEffect = typeof useIsomorphicLayoutEffect === 'function' @@ -66,16 +66,7 @@ export function Dropdown(props: PropsWithChildren): JSX.Element { alwaysSelectOption = false } = props; - //reset the id when the component is re-rendered - const [idFromHook] = useId(); - const [screenReaderUUID, setScreenReaderUUID] = useState(''); - - useLayoutEffect(() => { - if (!screenReaderUUID) { - setScreenReaderUUID(idFromHook); - } - }, [screenReaderUUID, idFromHook]); - + const screenReaderUUID = useId(); const containerRef = useRef(null); const [screenReaderKey, setScreenReaderKey] = useState(0); const [hasTyped, setHasTyped] = useState(false); diff --git a/src/components/Filters/CheckboxOption.tsx b/src/components/Filters/CheckboxOption.tsx index 7cea32111..0348f18f3 100644 --- a/src/components/Filters/CheckboxOption.tsx +++ b/src/components/Filters/CheckboxOption.tsx @@ -6,7 +6,7 @@ import { useComposedCssClasses } from '../../hooks'; import { findSelectableFieldValueFilter, isNumberRangeValue, getDefaultFilterDisplayName } from '../../utils/filterutils'; import classNames from 'classnames'; import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; -import { useId } from "react-id-generator"; +import { useId } from '../../hooks/useId'; /** * The configuration data for a field value filter option. @@ -85,16 +85,7 @@ export function CheckboxOption(props: CheckboxOptionProps): JSX.Element | null { resultsCount } = props; - //reset the id when the component is re-rendered - const [idFromHook] = useId(); - const [optionId, setOptionId] = useState(''); - - useLayoutEffect(() => { - if (!optionId) { - setOptionId(idFromHook); - } - }, [optionId, idFromHook]); - + const optionId = useId(); const cssClasses = useComposedCssClasses(builtInCssClasses, props.customCssClasses); const { selectFilter, filters, applyFilters } = useFiltersContext(); diff --git a/src/hooks/useId.ts b/src/hooks/useId.ts new file mode 100644 index 000000000..8094935e2 --- /dev/null +++ b/src/hooks/useId.ts @@ -0,0 +1,64 @@ +//Copied with minor modifications from https://github.com/reach/reach-ui/blob/dev/packages/auto-id/src/reach-auto-id.ts + + +import * as React from "react"; +import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; + +const useLayoutEffect = typeof useIsomorphicLayoutEffect === 'function' + ? useIsomorphicLayoutEffect + : useIsomorphicLayoutEffect['default']; + +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 empty string and will update after a + * component mounts. + * + * @see Docs https://reach.tech/auto-id + */ + +export function useId():string { + if (maybeReactUseId !== undefined) { + return maybeReactUseId(); + } + + // 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. + const initialId = (serverHandoffComplete ? genId() : ''); + const [id, setId] = React.useState(initialId); + + useLayoutEffect(() => { + if (id === '') { + // 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()); + } + }, [id]); + + 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 id.toString(); +} \ No newline at end of file diff --git a/tests/hooks/getRenderHook.ts b/tests/hooks/getRenderHook.ts index b0f261b57..bacc162b6 100644 --- a/tests/hooks/getRenderHook.ts +++ b/tests/hooks/getRenderHook.ts @@ -1,12 +1,10 @@ function getRenderHook() { try { - // Attempt to import the module + // Attempt to import testing-library/react-hooks const testingLibraryHooks = require('@testing-library/react-hooks'); return testingLibraryHooks.renderHook; } catch (error) { - // Handle the case where the module is not available - console.error('Unable to import @testing-library/react-hooks:', error); - // Fallback to using require + // Fallback to using testing-library/react return require('@testing-library/react').renderHook; } }