Skip to content

Commit

Permalink
fix: Make sure useId can be deterministic
Browse files Browse the repository at this point in the history
If the user provides an ID fallback to a component that calls useId, we
need to make sure the IDs are deterministic beyond the first render. We
we mistakenly used the same ID across renders, so this is fixed. this
also adds a check for React's internal useId hook in React 18 and
prioritizes it over our hack. We don't formally support React 18 yet but
this will help get us there.
  • Loading branch information
chaance committed Apr 20, 2022
1 parent 209c53f commit b2f3bc0
Showing 1 changed file with 26 additions and 21 deletions.
47 changes: 26 additions & 21 deletions packages/auto-id/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ let serverHandoffComplete = false;
let id = 0;
const genId = () => ++id;

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

/**
* useId
*
Expand All @@ -73,41 +75,44 @@ const genId = () => ++id;
* @see Docs https://reach.tech/auto-id
*/
function useId(idFromProps: string): string;
function useId(idFromProps: string | undefined): string | undefined;
function useId(idFromProps?: null): string | undefined;
function useId(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);
function useId(idFromProps: string | undefined | null): string | undefined;
function useId(): string | undefined;

function useId(providedId?: string | undefined | null) {
// TODO: Remove when updating internal deps to React 18
// @ts-expect-error
if (typeof React.useId === "function") {
// @ts-expect-error
let id = React.useId(providedId);
return providedId != null ? providedId : String(id);
}

const [id, setId] = React.useState(initialId);
// 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).
*/
// 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.
*/
// 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;

return providedId != null ? providedId : id != null ? String(id) : undefined;
}

export { useId };

0 comments on commit b2f3bc0

Please sign in to comment.