Skip to content

fix(useForwardExpose): sync currentElement on DOM update#2475

Open
kricsleo wants to merge 1 commit intounovue:v2from
kricsleo:fix/forward-ref
Open

fix(useForwardExpose): sync currentElement on DOM update#2475
kricsleo wants to merge 1 commit intounovue:v2from
kricsleo:fix/forward-ref

Conversation

@kricsleo
Copy link
Collaborator

@kricsleo kricsleo commented Feb 26, 2026

🔗 Linked issue

resolves #2474

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

📸 Screenshots (if appropriate)

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed element references not updating when conditional rendering swaps between different element types.
  • Tests

    • Added test coverage to verify element tracking updates correctly when conditional child elements change.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Modifies useForwardExpose to reactively update the current element reference when the DOM child element changes, such as during conditional element swaps. Introduces a resolveCurrentElement helper and an onUpdated lifecycle hook to synchronize element tracking. Includes a test verifying proper updates when an as-child component swaps between different element types.

Changes

Cohort / File(s) Summary
useForwardExpose Implementation
packages/core/src/shared/useForwardExpose.ts
Replaces static currentElement computation with reactive computed. Adds resolveCurrentElement helper to handle text/comment nodes and unwrapping. Integrates onUpdated hook to re-sync currentRef when DOM element changes during conditional rendering.
useForwardExpose Test Coverage
packages/core/src/shared/useForwardExpose.test.ts
Adds test validating that currentElement updates correctly when an as-child conditional child swaps between button and span elements. Uses watchPostEffect to track rendered element changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through conditional swaps,
Where elements dance and DOM nodes pop,
With watchPostEffect and hooks aligned,
The right element is always signed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(useForwardExpose): sync currentElement on DOM update' accurately describes the main change: implementing synchronization of the currentElement when the DOM element changes.
Linked Issues check ✅ Passed The PR addresses issue #2474 by making useForwardExpose reactive to DOM updates via onUpdated hook and resolveCurrentElement helper, ensuring Popover detects trigger element changes and repositions correctly.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing useForwardExpose to sync currentElement on DOM updates, with a test validating the fix. No extraneous modifications were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 26, 2026

Open in StackBlitz

npm i https://pkg.pr.new/reka-ui@2475

commit: c29f872

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/shared/useForwardExpose.test.ts (1)

299-324: Add one update-cycle test for direct DOM refs as well.

This case validates the asChild component-instance path well. Please add a companion case where forwardRef receives a plain Element and the component re-renders, so the new onUpdated sync path is protected end-to-end too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/shared/useForwardExpose.test.ts` around lines 299 - 324,
Add a companion test to the existing "should update currentElement when as-child
conditional child swaps" case that exercises the direct DOM ref path: mount a
component that uses useForwardExpose() and assigns forwardRef to a plain Element
(not a component instance) via asChild or direct ref, track
currentElement.value.nodeName in a post-effect (like renderNode), toggle between
two different DOM tags (e.g., 'button' and 'span'), flush promises, and assert
the nodeName updates accordingly; reference useForwardExpose, forwardRef,
currentElement, Primitive and the existing test pattern to mirror the setup and
toggling logic so the onUpdated sync path for direct Element refs is validated
end-to-end.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/shared/useForwardExpose.ts`:
- Around line 21-24: In resolveCurrentElement, guard the $el property before
reading nodeName to avoid crashes when forwardRef receives a plain DOM element:
check for currentRef.value?.$el (or use optional chaining
currentRef.value?.$el?.nodeName) before comparing against ['#text', '#comment'],
and fall back to unrefElement(currentRef) when $el is missing; update the
conditional in resolveCurrentElement (referencing currentRef, $el, nodeName,
nextElementSibling, and unrefElement) so it only accesses nodeName if $el exists
and returns the appropriate element otherwise.

---

Nitpick comments:
In `@packages/core/src/shared/useForwardExpose.test.ts`:
- Around line 299-324: Add a companion test to the existing "should update
currentElement when as-child conditional child swaps" case that exercises the
direct DOM ref path: mount a component that uses useForwardExpose() and assigns
forwardRef to a plain Element (not a component instance) via asChild or direct
ref, track currentElement.value.nodeName in a post-effect (like renderNode),
toggle between two different DOM tags (e.g., 'button' and 'span'), flush
promises, and assert the nodeName updates accordingly; reference
useForwardExpose, forwardRef, currentElement, Primitive and the existing test
pattern to mirror the setup and toggling logic so the onUpdated sync path for
direct Element refs is validated end-to-end.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78e762e and c29f872.

📒 Files selected for processing (2)
  • packages/core/src/shared/useForwardExpose.test.ts
  • packages/core/src/shared/useForwardExpose.ts

Comment on lines +21 to 24
function resolveCurrentElement(): HTMLElement {
// $el could be text/comment for non-single root normal or text root, thus we retrieve the nextElementSibling
// @ts-expect-error ignore ts error
return ['#text', '#comment'].includes(currentRef.value?.$el.nodeName) ? currentRef.value?.$el.nextElementSibling : unrefElement(currentRef)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n packages/core/src/shared/useForwardExpose.ts

Repository: unovue/reka-ui

Length of output: 3820


Guard $el before reading nodeName to avoid update-time crashes.

If forwardRef receives a plain DOM element, it lacks a $el property. The expression currentRef.value?.$el.nodeName only guards against null/undefined on currentRef.value itself, not on the undefined $el property that follows. This throws when accessed during the onUpdated hook triggered by component updates.

🐛 Proposed fix
 function resolveCurrentElement(): HTMLElement {
   // $el could be text/comment for non-single root normal or text root, thus we retrieve the nextElementSibling
   // `@ts-expect-error` ignore ts error
-  return ['#text', '#comment'].includes(currentRef.value?.$el.nodeName) ? currentRef.value?.$el.nextElementSibling : unrefElement(currentRef)
+  const target = currentRef.value instanceof Element
+    ? currentRef.value
+    : currentRef.value?.$el
+
+  if (!target)
+    return undefined
+
+  return ['#text', '#comment'].includes(target.nodeName)
+    ? target.nextElementSibling
+    : target
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/shared/useForwardExpose.ts` around lines 21 - 24, In
resolveCurrentElement, guard the $el property before reading nodeName to avoid
crashes when forwardRef receives a plain DOM element: check for
currentRef.value?.$el (or use optional chaining currentRef.value?.$el?.nodeName)
before comparing against ['#text', '#comment'], and fall back to
unrefElement(currentRef) when $el is missing; update the conditional in
resolveCurrentElement (referencing currentRef, $el, nodeName,
nextElementSibling, and unrefElement) so it only accesses nodeName if $el exists
and returns the appropriate element otherwise.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Popover doesn't react to trigger child changing when using as-child

1 participant