Skip to content

Replace forwarded ref instances of useProvidedRefOrCreate with useMergedRefs#7644

Open
iansan5653 wants to merge 19 commits intomainfrom
replace-use-provided-ref-or-create
Open

Replace forwarded ref instances of useProvidedRefOrCreate with useMergedRefs#7644
iansan5653 wants to merge 19 commits intomainfrom
replace-use-provided-ref-or-create

Conversation

@iansan5653
Copy link
Contributor

@iansan5653 iansan5653 commented Mar 9, 2026

In #7638 I created a useMergedRefs hook and migrated all instances of useRefObjectAsForwardedRef to it.

A similar pattern is using useProvidedRefOrCreate for this. It's typically used with a type cast and a ts-expect-error comment (❗):

const ref = useProvidedRefOrCreate(forwardedRef as RefObject<HTMLElement | null>)

// @ts-expect-error
return <div ref={ref} />

This is obviously problematic. The type errors are trying to point out the problem with this pattern: it will break if consumers pass ref callbacks. Ref callbacks are likely to become more and more common as React 19 has introduced ref callback cleanup functions, making ref callbacks a viable alternative to effects.

A much better pattern is to be consistent and use the same approach as in #7638:

const ref = useRef<HTMLElement>(null)
const combinedRef = useCombinedRefs(ref, forwardedRef)

return <div ref={combinedRef} />

Note, however, that I did not deprecate useProvidedRefOrCreate or remove all calls to it. There are still some valid use cases where refs can be passed in to refer to elements rendered outside the component. We most often see this in anchor refs. There are also some utility hooks that can create a ref internally or use a passed ref. These two cases are still valid; the case I'm targeting here is specifically around forwarded refs.

Changelog

New

Changed

Removed

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Mar 9, 2026

⚠️ No Changeset found

Latest commit: c0612a7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Mar 9, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot requested a deployment to storybook-preview-7644 March 9, 2026 15:33 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7644 March 9, 2026 15:43 Inactive
@github-actions github-actions bot requested a deployment to storybook-preview-7644 March 9, 2026 15:51 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7644 March 9, 2026 16:01 Inactive
@iansan5653 iansan5653 force-pushed the replace-use-provided-ref-or-create branch from 422c4e4 to 006cc27 Compare March 17, 2026 17:01
@iansan5653 iansan5653 changed the base branch from create-use-combined-refs to update-use-merged-refs March 17, 2026 17:01
Base automatically changed from update-use-merged-refs to main March 18, 2026 19:40
@github-actions github-actions bot requested a deployment to storybook-preview-7644 March 24, 2026 18:55 Abandoned
@primer
Copy link
Contributor

primer bot commented Mar 24, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

@iansan5653 iansan5653 added the skip changeset This change does not need a changelog label Mar 24, 2026
@iansan5653
Copy link
Contributor Author

Skipping changeset because the public facing changes are covered by the changeset in #7638

@iansan5653 iansan5653 marked this pull request as ready for review March 24, 2026 19:01
@iansan5653 iansan5653 requested a review from a team as a code owner March 24, 2026 19:01
@github-actions github-actions bot requested a deployment to storybook-preview-7644 March 24, 2026 19:02 Abandoned
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR continues the ref-modernization work in @primer/react by deprecating useProvidedRefOrCreate (problematic with callback refs) and migrating most component usage to useMergedRefs, aligning the codebase with React 19–compatible ref patterns.

Changes:

  • Replace useProvidedRefOrCreate usages in many components with an internal useRef + useMergedRefs pattern to support callback refs and remove @ts-expect-error ref casts.
  • Update AnchoredOverlay to accept React.Ref types and use useMergedRefs for anchor/overlay refs.
  • Minor doc/test variable renames to reflect “merged” terminology.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/react/src/hooks/useMergedRefs.ts Update examples to use mergedRef naming in docs.
packages/react/src/hooks/useFocusTrap.ts Import reordering / minor formatting while retaining useProvidedRefOrCreate for hook-specific behavior.
packages/react/src/hooks/useAnchoredPosition.ts Import reordering / minor formatting while retaining useProvidedRefOrCreate for hook-specific behavior.
packages/react/src/hooks/tests/useMergedRefs.test.tsx Rename local variable from combinedRef to mergedRef.
packages/react/src/experimental/Tabs/Tabs.tsx Migrate tablist ref handling to useRef + useMergedRefs and remove ts-expect-error.
packages/react/src/experimental/Tabs/Tabs.examples.stories.tsx Remove ts-expect-error tied to non-nullable ref typing.
packages/react/src/experimental/SelectPanel2/SelectPanel.tsx Migrate anchor ref wiring to useRef + useMergedRefs.
packages/react/src/deprecated/ActionMenu.tsx Migrate anchored overlay anchor ref wiring to useRef + useMergedRefs.
packages/react/src/TooltipV2/Tooltip.tsx Migrate trigger ref wiring to useRef + useMergedRefs; widen trigger ref typing.
packages/react/src/TextInput/TextInput.tsx Migrate input ref wiring to useRef + useMergedRefs; remove ts-expect-error.
packages/react/src/SelectPanel/SelectPanel.tsx Migrate anchor ref wiring to useRef + useMergedRefs.
packages/react/src/PageHeader/PageHeader.tsx Migrate root ref handling to useRef + useMergedRefs; simplify TitleArea to use forwarded ref directly.
packages/react/src/FilteredActionList/FilteredActionList.tsx Migrate scroll container + input refs to useRef + useMergedRefs; remove ts-expect-error.
packages/react/src/Dialog/Dialog.tsx Migrate autoFocus button ref wiring to useRef + useMergedRefs; remove ts-expect-error.
packages/react/src/Checkbox/Checkbox.tsx Migrate input ref wiring to useRef + useMergedRefs; remove ts-expect-error.
packages/react/src/ButtonGroup/ButtonGroup.tsx Migrate container ref wiring to useRef + useMergedRefs; remove ts-expect-error.
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx Change anchorRef prop typing to React.Ref, merge overlay refs with useMergedRefs, remove custom assignRef.
packages/react/src/ActionMenu/ActionMenu.tsx Migrate internal anchor ref handling to useRef + useMergedRefs for composable ActionMenu.
packages/react/src/ActionList/List.tsx Migrate list ref wiring to useRef + useMergedRefs; remove ts-expect-error.
Comments suppressed due to low confidence (1)

packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx:221

  • AnchoredOverlay now always uses an internal anchorRef for useAnchoredPosition, returnFocusRef, and ignoreClickRefs. When renderAnchor is null, that internal ref is never attached to any element, so anchorRef.current stays null and the overlay will never position/show. In the renderAnchor: null branch, use the provided anchorRef (and keep that prop typed as a RefObject) for positioning/focus management, and only use the merged ref when renderAnchor is provided.
  const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning')
  const anchorRef = useRef<HTMLElement>(null)
  const mergedRef = useMergedRefs(anchorRef, externalAnchorRef)

  const [overlayRef, updateOverlayRef] = useRenderForcingRef<HTMLDivElement>()
  const mergedOverlayRef = useMergedRefs(updateOverlayRef, overlayProps?.ref)

  const anchorId = useId(externalAnchorId)

  const onClickOutside = useCallback(() => onClose?.('click-outside'), [onClose])
  const onEscape = useCallback(() => onClose?.('escape'), [onClose])

  const onAnchorKeyDown = useCallback(
    (event: React.KeyboardEvent<HTMLElement>) => {
      if (!event.defaultPrevented) {
        if (!open && ['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key)) {
          onOpen?.('anchor-key-press', event)
          event.preventDefault()
        }
      }
    },
    [open, onOpen],
  )
  const onAnchorClick = useCallback(
    (event: React.MouseEvent<HTMLElement>) => {
      if (event.defaultPrevented || event.button !== 0) {
        return
      }
      if (!open) {
        onOpen?.('anchor-click')
      } else {
        onClose?.('anchor-click')
      }
    },
    [open, onOpen, onClose],
  )

  const positionChange = (position: AnchorPosition | undefined) => {
    if (onPositionChange && position) {
      onPositionChange({position})
    }
  }

  const {position} = useAnchoredPosition(
    {
      anchorElementRef: anchorRef,
      floatingElementRef: overlayRef,

@iansan5653 iansan5653 changed the title Deprecate useProvidedRefOrCreate and migrate everything except for hooks Replace forwarded ref instances of useProvidedRefOrCreate with useMergedRefs Mar 24, 2026
// Only works in React 19. In React 18, the cleanup function will be ignored and the ref will get called with
// Callback refs only work in React 19+. In React 18, the ref will get called with
// `null` which will be passed to each ref as expected.
if (majorReactVersion <= 18) return
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting a test failure because of an extra call to console.error in React 18 tests. Things are working as expected but React warns when a cleanup function is returned from a callback ref in versions before 19, because it's trying to future-proof for 19 and not cause unexpected side effects.

To resolve the warning, I just inspect the version of React explicitly instead of relying on the implicit differences in behavior. When we drop 19 support we can remove this.

@github-actions github-actions bot temporarily deployed to storybook-preview-7644 March 24, 2026 19:54 Inactive
@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/16899

@primer-integration
Copy link

Integration test results from github/github-ui:

Failed  CI   Failed
Waiting  VRT   Waiting
Waiting  Projects   Waiting

CI check runs linting, type checking, and unit tests. Check the workflow logs for specific failures.

Need help? If you believe this failure is unrelated to your changes, please reach out to the Primer team for assistance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm skip changeset This change does not need a changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants