dequelabs / dequelabs/cauldron
Bug: useSharedRef leaves a stale element reference on the parent ref after unmount
- Dominant language
- TypeScript
- Stars
- 127
- Forks
- 31
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 8
Description
## Problem
`useSharedRef` ([packages/react/src/utils/useSharedRef.ts](https://github.com/dequelabs/cauldron/blob/develop/packages/react/src/utils/useSharedRef.ts)) forwards the internal element to the parent ref via a `useEffect` keyed on `[ref]`, but it has no cleanup. When the host component unmounts, React clears the internal `MutableRefObject`, but the *parent's* ref is never updated — so a parent holding a function ref or a `MutableRefObject` is left pointing at the now-detached DOM node.
A class-style inline ref callback (`ref={(el) => setRef(parent, el)}`) gets called with `null` on unmount and forwards that to the parent. `useSharedRef` does not.
## Impact
Any consumer that uses `toastRef` / `drawerRef` / `dialogRef` / etc. for lifecycle tracking (e.g., `if (ref.current) { … }`) gets a falsy-positive after unmount — `ref.current` is truthy but points at a detached node. The bug compounds for components with portal logic or that conditionally mount, where consumers expect ref-clearing as a signal.
This affects every component that uses `useSharedRef` today: Toast, Drawer, Dialog, Combobox, Listbox, AnchoredOverlay, LoaderOverlay, CopyButton, TextEllipsis, and ComboboxOption.
## Proposal
Add a cleanup that propagates `null` to the parent ref on unmount:
```ts
export default function useSharedRef(ref: Ref): MutableRefObject {
const internalRef = useRef();
useEffect(() => {
setRef(ref, internalRef.current);
return () => setRef(ref, null);
}, [ref]);
return internalRef as MutableRefObject;
}
```
Add a test in `useSharedRef.test.tsx` that unmounts and asserts the external ref reads `null`.
## Surfacing
Surfaced during review of #2349 and originally raised by Copilot on that PR.
Contributor guide
Assessment
This issue has not been assessed yet.