diff --git a/src/__tests__/utils.test.tsx b/src/__tests__/utils.test.tsx
index 2439cef..20ffb50 100644
--- a/src/__tests__/utils.test.tsx
+++ b/src/__tests__/utils.test.tsx
@@ -1,7 +1,41 @@
import React from 'react';
-import { useEffectAfterMount, useControlledState, callAll } from '../utils';
+import {
+ useEffectAfterMount,
+ useControlledState,
+ callAll,
+ useUniqueId,
+} from '../utils';
import { render, act } from '@testing-library/react';
+describe('useUniqueId', () => {
+ it('should generate a unique ID value', () => {
+ function Comp() {
+ const justNull = null;
+ const randId = useUniqueId(justNull);
+ const randId2 = useUniqueId();
+ return (
+
+ );
+ }
+ const { getByText } = render();
+ const id1 = Number(getByText('Wow').id);
+ const id2 = Number(getByText('Ok').id);
+ expect(id2).not.toEqual(id1);
+ });
+
+ it('uses a fallback ID', () => {
+ function Comp() {
+ const newId = useUniqueId('awesome');
+ return Ok
;
+ }
+ const { getByText } = render();
+ expect(getByText('Ok').id).toEqual('awesome');
+ });
+});
+
describe('callAll', () => {
it('it calls the two functions passed into it', () => {
const functionOne = jest.fn();
diff --git a/src/utils.ts b/src/utils.ts
index 29cba94..3e03b7a 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,4 +1,11 @@
-import { RefObject, useState, useRef, useEffect, useCallback } from 'react';
+import {
+ RefObject,
+ useState,
+ useRef,
+ useEffect,
+ useCallback,
+ useLayoutEffect,
+} from 'react';
import warning from 'tiny-warning';
import { AssignableRef } from './types';
@@ -118,19 +125,56 @@ export function useEffectAfterMount(
}, dependencies);
}
-// Unique ID implementation borrowed from React UI :)
-// https://github.com/reach/reach-ui/blob/6e9dbcf716d5c9a3420e062e5bac1ac4671d01cb/packages/auto-id/src/index.js
-let idCounter = 0;
-const genId = (): number => ++idCounter;
-
/**
- * This generates a unique ID for an instance of Collapse
- * @return {String} the unique ID
+ * Taken from Reach
+ * https://github.com/reach/reach-ui/blob/d2b88c50caf52f473a7d20a4493e39e3c5e95b7b/packages/auto-id
+ *
+ * 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
*/
-export function useUniqueId(): number {
- const [id, setId] = useState(0);
- useEffect(() => setId(genId()), []);
- return id;
+const useIsomorphicLayoutEffect =
+ typeof window !== 'undefined' ? useEffect : useLayoutEffect;
+let serverHandoffComplete = false;
+let id = 0;
+const genId = () => ++id;
+export function useUniqueId(idFromProps?: string | null) {
+ /*
+ * 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 = idFromProps || (serverHandoffComplete ? genId() : null);
+
+ const [id, setId] = 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
+ }, []);
+
+ 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 != null ? String(id) : undefined;
}
export function usePaddingWarning(element: RefObject): void {