-
-
Notifications
You must be signed in to change notification settings - Fork 173
fix(directives): Robustness fixes for directives #2906
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughThe PR centralizes floating directive behavior via a new createFloatingDirective factory, introduces per-directive UID-scoped instance state, adds SSR guards and safer cleanup for floating UI, updates BPopover/BTooltip to use the factory, namespaces BScrollspy instances per-UID, and hardens BToggle retry/unmount logic. (49 words) Changes
Sequence Diagram(s)sequenceDiagram
participant App as Vue App
participant DirectiveFactory as createFloatingDirective
participant Utils as Directive Utils
participant Component as Floating Component
participant DOM as DOM
rect rgb(220,240,255)
note over App,DirectiveFactory: Mount
App->>DirectiveFactory: mounted(el,binding)
DirectiveFactory->>Utils: getDirectiveUid(binding)
Utils-->>DirectiveFactory: uid
DirectiveFactory->>Utils: initDirectiveInstance(el,name,uid,binding)
DirectiveFactory->>DirectiveFactory: resolveContent & buildProps
DirectiveFactory->>Component: bind(el, props)
Component->>DOM: create & mount floating element
end
rect rgb(220,255,220)
note over App,DirectiveFactory: Update
App->>DirectiveFactory: updated(el,binding)
DirectiveFactory->>Utils: getDirectiveInstance(el,name,uid)
DirectiveFactory->>Utils: hasBindingChanged(instance,binding)
alt changed
DirectiveFactory->>Component: unbind(el)
DirectiveFactory->>DirectiveFactory: buildProps & bind(el, props)
DirectiveFactory->>Utils: updateBindingCache(instance,binding)
end
end
rect rgb(255,230,230)
note over App,DirectiveFactory: Unmount
App->>DirectiveFactory: beforeUnmount(el,binding)
DirectiveFactory->>Component: unbind(el)
DirectiveFactory->>Utils: cleanupDirectiveInstance(el,name,uid)
Utils->>DOM: remove instance & mark destroying
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (7)📓 Common learnings📚 Learning: 2025-09-12T14:46:49.416ZApplied to files:
📚 Learning: 2025-05-28T07:01:55.095ZApplied to files:
📚 Learning: 2025-08-19T23:30:07.062ZApplied to files:
📚 Learning: 2025-06-26T19:46:19.333ZApplied to files:
📚 Learning: 2025-08-19T23:30:07.062ZApplied to files:
📚 Learning: 2025-04-28T22:48:46.738ZApplied to files:
🧬 Code graph analysis (1)packages/bootstrap-vue-next/src/utils/floatingUi.ts (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (4)
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. Comment |
commit: |
There was a problem hiding this 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 implements critical robustness fixes for Vue directives, addressing race conditions, memory leaks, and multi-instance collision issues. The changes introduce UID-based namespacing (following Vuetify's pattern), improved cleanup logic with queueMicrotask, SSR guards, and a refactored architecture using a shared createFloatingDirective factory function.
Key changes:
- Implemented UID namespacing for directive state management to prevent cross-instance contamination
- Refactored tooltip/popover directives to use a shared
createFloatingDirectivefactory - Added race condition protection in BToggle's async registration loop with mount state checks
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
packages/bootstrap-vue-next/src/utils/floatingUi.ts |
Added SSR guards, updated unbind to use queueMicrotask, modified TypeScript interfaces for UID namespacing, improved resolveContent SSR handling |
packages/bootstrap-vue-next/src/directives/utils.ts |
Added new UID-based state management utilities, created createFloatingDirective factory function with improved lifecycle handling |
packages/bootstrap-vue-next/src/directives/BTooltip/index.ts |
Refactored to use createFloatingDirective factory, reducing code duplication |
packages/bootstrap-vue-next/src/directives/BPopover/index.ts |
Refactored to use createFloatingDirective factory, reducing code duplication |
packages/bootstrap-vue-next/src/directives/BScrollspy/index.ts |
Implemented UID namespacing for multi-instance safety with SSR guards |
packages/bootstrap-vue-next/src/directives/BToggle/index.ts |
Fixed async registration race conditions with additional mount state checks throughout the loop |
Comments suppressed due to low confidence (2)
packages/bootstrap-vue-next/src/directives/BToggle/index.ts:101
- The async registration loop creates a potential race condition. If
handleUpdateis called multiple times rapidly (e.g., during quick re-renders), multiple async loops can run concurrently for the same target, each attempting to register/unregister triggers. This could lead to:
- Multiple registrations of the same trigger
- Unregistration calls being interleaved unpredictably
- The element state (
dataset.bvtoggle) being modified by concurrent iterations
Consider tracking pending registrations per element/target pair and canceling previous registration attempts when a new update occurs, or using a debounce/throttle mechanism for rapid updates.
targets.forEach(async (targetId) => {
let count = 0
const maxAttempts = 5
const delayMs = 100
// Keep looking until showHide is found, giving up after 500ms or directive is unmounted
while (count < maxAttempts) {
// Check if element is still mounted before each iteration
if (!(el as HTMLElement).dataset.bvtoggle) {
// Element was unmounted, stop trying
return
}
const showHide = showHideMap?.value.get(targetId)
if (!showHide) {
count++
if (count < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, delayMs))
continue
}
// Only warn if element is still mounted after all attempts
if ((el as HTMLElement).dataset.bvtoggle) {
// eslint-disable-next-line no-console
console.warn(
`[v-b-toggle] Target with ID ${targetId} not found after ${maxAttempts * delayMs}ms`
)
}
break
}
// Final check before registration
if (!(el as HTMLElement).dataset.bvtoggle) return
// Register the trigger element
toValue(showHide).unregisterTrigger('click', el, false)
toValue(showHide).registerTrigger('click', el)
break
}
})
packages/bootstrap-vue-next/src/utils/floatingUi.ts:142
- The
bindfunction doesn't support multiple component instances using directives on the same element. Theel.$__elementproperty (line 142) is shared across all instances, so when multiple component instances try to create tooltips/popovers on the same element:
- The first instance creates
el.$__element - The second instance overwrites
el.$__element, orphaning the first instance's DOM element - When instances unbind, they may remove each other's elements
This breaks the UID namespacing pattern used elsewhere. The $__element should be namespaced per UID like $__tooltip and $__popover, or the architecture should ensure only one visual element exists per HTML element regardless of how many component instances reference it.
Consider either:
- Making
$__elementUID-namespaced:el.$__elements[uid] = div - Or enforcing a single tooltip/popover per element with proper reference counting
const div = document.createElement('span')
if (binding.modifiers.body) document.body.appendChild(div)
else if (binding.modifiers.child) el.appendChild(div)
else el.parentNode?.insertBefore(div, el.nextSibling)
render(h(BPopover, props), div)
el.$__element = div
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
packages/bootstrap-vue-next/src/directives/BToggle/index.ts (2)
63-101: Consider Promise.all for better error handling and testability.The
forEachwith an async callback meanshandleUpdatereturns immediately without waiting for any registration attempts to complete. This is a common async antipattern that prevents error aggregation and makes it difficult to test when registrations are complete.If non-blocking registration is intentional, consider adding a comment to document this behavior. Otherwise, consider refactoring to:
- targets.forEach(async (targetId) => { + await Promise.all(targets.map(async (targetId) => { // ... retry logic ... - }) + }))This would allow the directive lifecycle to complete registrations before the next update or unmount, reducing race condition windows.
65-66: Consider extracting retry configuration to module level.The retry constants are defined inside the forEach callback, creating new constants for each target on every update. While the performance impact is negligible, extracting them to the module level would improve clarity and make them easier to adjust:
+const MAX_REGISTRATION_ATTEMPTS = 5 +const REGISTRATION_DELAY_MS = 100 + const handleUpdate = ( // ... ) => { targets.forEach(async (targetId) => { let count = 0 - const maxAttempts = 5 - const delayMs = 100 + const maxAttempts = MAX_REGISTRATION_ATTEMPTS + const delayMs = REGISTRATION_DELAY_MSpackages/bootstrap-vue-next/src/directives/utils.ts (1)
24-37: Consider defensive handling for binding.instance access.Line 36 uses a non-null assertion (
binding.instance!.$.uid) which assumes the directive is always used with a component instance. While this is typically safe in Vue 3 directive context, consider adding a defensive check or documenting the assumption.Apply this diff if defensive handling is desired:
export function getDirectiveUid(binding: DirectiveBinding): number { + if (!binding.instance) { + throw new Error('Directive must be used within a component instance') + } return binding.instance!.$.uid }Alternatively, document the assumption in the JSDoc comment.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
packages/bootstrap-vue-next/src/directives/BPopover/index.ts(1 hunks)packages/bootstrap-vue-next/src/directives/BScrollspy/index.ts(2 hunks)packages/bootstrap-vue-next/src/directives/BToggle/index.ts(1 hunks)packages/bootstrap-vue-next/src/directives/BTooltip/index.ts(1 hunks)packages/bootstrap-vue-next/src/directives/utils.ts(3 hunks)packages/bootstrap-vue-next/src/utils/floatingUi.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2777
File: packages/bootstrap-vue-next/CHANGELOG.md:11-11
Timestamp: 2025-08-18T18:20:08.240Z
Learning: For PR #2777 (BSort updates), keep changes scoped to BTable sorting work; unrelated edits like CHANGELOG typos should be deferred to a separate PR/issue.
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('popover') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this prop spreading/inheritance mechanism.
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.095Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('tooltip') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this inheritance mechanism.
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2425
File: packages/bootstrap-vue-next/src/plugins/popoverController/index.ts:138-140
Timestamp: 2025-04-28T22:48:46.738Z
Learning: In bootstrap-vue-next, `PopoverOrchestratorParam` includes an `id` field of type `ControllerKey` through inheritance from base types. This field is declared in the `BvControllerOptions` interface and propagated through the type hierarchy.
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:0-0
Timestamp: 2025-06-26T19:46:19.333Z
Learning: BTooltip is a very thin wrapper around BPopover in bootstrap-vue-next. There is no separate `useTooltipController` composable - the `usePopoverController` composable can be used to programmatically control both popovers and tooltips.
📚 Learning: 2025-08-19T23:30:07.062Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('popover') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this prop spreading/inheritance mechanism.
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/utils.tspackages/bootstrap-vue-next/src/directives/BToggle/index.tspackages/bootstrap-vue-next/src/utils/floatingUi.ts
📚 Learning: 2025-08-19T23:30:07.062Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('tooltip') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this inheritance mechanism.
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/utils.tspackages/bootstrap-vue-next/src/directives/BToggle/index.tspackages/bootstrap-vue-next/src/utils/floatingUi.ts
📚 Learning: 2025-06-26T19:46:19.333Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:0-0
Timestamp: 2025-06-26T19:46:19.333Z
Learning: BTooltip is a very thin wrapper around BPopover in bootstrap-vue-next. There is no separate `useTooltipController` composable - the `usePopoverController` composable can be used to programmatically control both popovers and tooltips.
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/BToggle/index.ts
📚 Learning: 2025-09-12T14:46:49.416Z
Learnt from: CR
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-12T14:46:49.416Z
Learning: Applies to packages/bootstrap-vue-next/src/components/index.ts : When adding a new component, update packages/bootstrap-vue-next/src/components/index.ts to export it
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/utils.tspackages/bootstrap-vue-next/src/directives/BScrollspy/index.tspackages/bootstrap-vue-next/src/directives/BToggle/index.tspackages/bootstrap-vue-next/src/utils/floatingUi.ts
📚 Learning: 2025-09-12T14:46:49.416Z
Learnt from: CR
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-12T14:46:49.416Z
Learning: Applies to packages/bootstrap-vue-next/src/App.vue : Use packages/bootstrap-vue-next/src/App.vue as a local test area for component changes
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/utils.tspackages/bootstrap-vue-next/src/directives/BScrollspy/index.tspackages/bootstrap-vue-next/src/directives/BToggle/index.tspackages/bootstrap-vue-next/src/utils/floatingUi.ts
📚 Learning: 2025-09-30T23:57:21.526Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2866
File: apps/docs/src/docs/components/demo/ModalModel.vue:11-15
Timestamp: 2025-09-30T23:57:21.526Z
Learning: The bootstrap-vue-next documentation project uses unplugin-vue-components to automatically resolve and import Vue components like BModal, BButton, etc. Explicit imports for these components in script sections are not required.
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-04-28T22:48:46.738Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2425
File: packages/bootstrap-vue-next/src/plugins/popoverController/index.ts:138-140
Timestamp: 2025-04-28T22:48:46.738Z
Learning: In bootstrap-vue-next, `PopoverOrchestratorParam` includes an `id` field of type `ControllerKey` through inheritance from base types. This field is declared in the `BvControllerOptions` interface and propagated through the type hierarchy.
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/utils.tspackages/bootstrap-vue-next/src/utils/floatingUi.ts
📚 Learning: 2025-09-12T14:46:49.416Z
Learnt from: CR
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-12T14:46:49.416Z
Learning: Applies to packages/bootstrap-vue-next/src/components/** : Create and modify Vue components only under packages/bootstrap-vue-next/src/components/
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/BToggle/index.ts
📚 Learning: 2025-08-19T14:23:46.775Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2732
File: packages/bootstrap-vue-next/CHANGELOG.md:35-41
Timestamp: 2025-08-19T14:23:46.775Z
Learning: In the bootstrap-vue-next repository, CHANGELOG.md files (e.g., packages/bootstrap-vue-next/CHANGELOG.md) are autogenerated and should be ignored in reviews; do not propose edits for them.
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BToggle/index.ts
📚 Learning: 2025-05-23T23:58:07.165Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2701
File: apps/docs/src/docs/migration-guide.md:630-632
Timestamp: 2025-05-23T23:58:07.165Z
Learning: The `<NotYetImplemented/>` component in the bootstrap-vue-next documentation automatically renders text indicating "Not Yet Implemented", so additional explanatory text about features not being implemented is redundant when this component is used.
Applied to files:
packages/bootstrap-vue-next/src/directives/BTooltip/index.tspackages/bootstrap-vue-next/src/directives/BPopover/index.tspackages/bootstrap-vue-next/src/directives/BToggle/index.ts
📚 Learning: 2025-10-21T19:31:54.113Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2882
File: apps/docs/src/docs/components/demo/PlaceholderWidth.vue:1-22
Timestamp: 2025-10-21T19:31:54.113Z
Learning: The bootstrap-vue-next project uses unplugin to automatically import components, so explicit imports in demo/documentation components are not needed.
Applied to files:
packages/bootstrap-vue-next/src/directives/BPopover/index.ts
📚 Learning: 2025-05-01T23:40:56.146Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2679
File: apps/docs/.vitepress/theme/Layout.vue:0-0
Timestamp: 2025-05-01T23:40:56.146Z
Learning: In bootstrap-vue-next, the `useScrollspy` hook returns an object with a `current` property which is the ID string (or null) of the currently active item, not an object containing an ID.
Applied to files:
packages/bootstrap-vue-next/src/directives/BScrollspy/index.ts
📚 Learning: 2025-05-28T07:01:55.095Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.095Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
Applied to files:
packages/bootstrap-vue-next/src/directives/BScrollspy/index.tspackages/bootstrap-vue-next/src/directives/BToggle/index.tspackages/bootstrap-vue-next/src/utils/floatingUi.ts
📚 Learning: 2025-05-28T07:57:19.915Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:384-404
Timestamp: 2025-05-28T07:57:19.915Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the activeIndex and activeId watchers are intentionally designed with separation of concerns: activeIndex watcher handles activate-tab event emission and complex validation logic, while activeId watcher is kept simple for synchronization between activeId and activeIndex values only.
Applied to files:
packages/bootstrap-vue-next/src/directives/BToggle/index.ts
🧬 Code graph analysis (5)
packages/bootstrap-vue-next/src/directives/BTooltip/index.ts (2)
packages/bootstrap-vue-next/src/utils/floatingUi.ts (2)
ElementWithPopper(110-127)resolveDirectiveProps(85-108)packages/bootstrap-vue-next/src/directives/utils.ts (1)
createFloatingDirective(197-296)
packages/bootstrap-vue-next/src/directives/BPopover/index.ts (2)
packages/bootstrap-vue-next/src/utils/floatingUi.ts (2)
ElementWithPopper(110-127)resolveDirectiveProps(85-108)packages/bootstrap-vue-next/src/directives/utils.ts (1)
createFloatingDirective(197-296)
packages/bootstrap-vue-next/src/directives/utils.ts (3)
packages/bootstrap-vue-next/src/types/ComponentProps.ts (1)
BPopoverProps(1334-1370)packages/bootstrap-vue-next/src/utils/floatingUi.ts (6)
ElementWithPopper(110-127)resolveActiveStatus(35-36)resolveContent(38-83)resolveDirectiveProps(85-108)bind(129-143)unbind(145-166)packages/bootstrap-vue-next/src/utils/keys.ts (1)
defaultsKey(204-205)
packages/bootstrap-vue-next/src/directives/BScrollspy/index.ts (3)
packages/bootstrap-vue-next/src/composables/useScrollspy/index.ts (1)
useScrollspy(44-236)packages/bootstrap-vue-next/src/directives/utils.ts (1)
getDirectiveUid(35-37)packages/bootstrap-vue-next/src/utils/object.ts (1)
omit(5-14)
packages/bootstrap-vue-next/src/utils/floatingUi.ts (1)
packages/bootstrap-vue-next/src/types/ComponentProps.ts (1)
BPopoverProps(1334-1370)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Agent
- GitHub Check: build
🔇 Additional comments (18)
packages/bootstrap-vue-next/src/directives/BToggle/index.ts (3)
69-74: Good defensive check against registration on unmounted elements.The mounted state check using
dataset.bvtoggleeffectively prevents race conditions when elements unmount during retry loops. This is a solid robustness improvement.
79-89: Excellent warning logic that reduces false positives.The conditional warning (lines 84-89) only fires when the element is still mounted after all retry attempts, preventing noisy warnings when elements are unmounted during the retry window. The warning message clearly communicates the timeout duration.
93-98: Good final safeguard before registration.The mounted check immediately before registration (line 94) minimizes the race condition window. Combined with the unregister-before-register pattern (line 97), this provides robust handling of edge cases.
packages/bootstrap-vue-next/src/directives/BTooltip/index.ts (1)
1-15: LGTM! Clean refactoring using the centralized factory pattern.The migration to
createFloatingDirectivesuccessfully consolidates the tooltip directive logic while maintaining correct behavior. Thenoninteractive: truedefault andtooltip: trueflag properly distinguish tooltips from popovers, and the title fallback chain (text.title ?? text.body ?? '') ensures content is always available.packages/bootstrap-vue-next/src/directives/BScrollspy/index.ts (3)
11-12: SSR guard properly placed.The early return prevents DOM access on the server, which is correct for scrollspy functionality that requires browser APIs.
14-24: Per-instance UID management correctly implemented.The UID namespacing enables multiple scrollspy instances per element (e.g., when the same element is used in different component instances). The cleanup of existing instances before creating new ones prevents memory leaks during updates.
47-56: Cleanup logic is correct and properly scoped.The beforeUnmount hook correctly targets only the instance associated with the current UID, preventing interference with other component instances that may be using the same element.
packages/bootstrap-vue-next/src/directives/BPopover/index.ts (1)
1-13: LGTM! Popover directive successfully refactored.The migration to
createFloatingDirectiveis clean and correct. The prop merging order (defaults → directive props → text) ensures proper precedence, and popovers correctly maintain their interactive behavior by default (unlike tooltips which setnoninteractive: true).packages/bootstrap-vue-next/src/utils/floatingUi.ts (4)
49-66: SSR guards correctly implemented for DOM attribute access.The guards properly prevent
getAttribute/setAttribute/removeAttributecalls on the server while maintaining equivalent behavior. The fallback path returns empty content when no binding value is provided in SSR context, which is appropriate.
134-135: SSR guard prevents DOM manipulation on server.Correctly skips
document.createElementand related DOM operations when running on the server.
145-165: Improved cleanup sequence with better performance characteristics.The refactored unbind sequence is solid:
- Immediate component unmount via
render(null, div)prevents lingering reactivityqueueMicrotaskprovides more predictable timing thansetTimeout(0)and better performance- SSR guard prevents DOM operations on server
- DOM removal and state cleanup in microtask are appropriately deferred
The comment on Line 162 correctly notes that UID-specific state cleanup happens in the directive's beforeUnmount.
110-126: ElementWithPopper interface properly extended for UID-namespaced state.The interface changes correctly support the new per-instance architecture:
- Index signature enables dynamic property assignment for directive state
- Removed
$__bindingin favor of per-UID state within$__tooltip/$__popovermaps- The
destroyingflag in each instance prevents race conditions during concurrent updates and cleanuppackages/bootstrap-vue-next/src/directives/utils.ts (6)
47-65: State initialization correctly prevents prototype pollution.Using
Object.create(null)for the UID namespace map is a good security practice that prevents prototype pollution attacks and avoids unexpected property inheritance.
85-109: Binding change detection via JSON serialization is pragmatic.The JSON.stringify approach for deep comparison is straightforward and handles nested objects/arrays correctly. While not the most performant for large binding values, it's acceptable here since:
- Directive bindings are typically small objects
- The comparison only runs on updates, not renders
- The cost is outweighed by preventing unnecessary rebinds
111-130: Cleanup properly coordinates with the destroying flag.Setting
destroying: truebefore deleting the instance entry ensures any in-flight updates will see the flag and abort, preventing race conditions during concurrent update/unmount scenarios.
208-231: Mounted lifecycle correctly guards against inactive or empty content.The early returns for inactive bindings (Line 213) and missing content (Line 217) prevent unnecessary DOM operations and state initialization. The order is optimal: check activation → resolve content → check content existence → initialize state → bind.
233-284: Updated lifecycle robustly handles state transitions and race conditions.The update path correctly handles multiple scenarios:
- Instance never mounted or already cleaned up (Line 238)
- Inactive binding → cleanup (Lines 246-251)
- Empty content → cleanup (Lines 255-261)
- No binding change → no-op (Line 265)
- Destroying flag → prevent race (Line 268)
The
delete binding.oldValueon Line 263 is intentional to prevent Vue's internal change detection from interfering with the custom binding comparison logic.
286-296: BeforeUnmount correctly cleans up per-instance state.The hook properly unbinds the UI element and removes the UID-specific state entry, ensuring no memory leaks when components are destroyed.
xvaara
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems a nice update, didn't test it but I presume it works :)
Addresses Copilot feedback on PR bootstrap-vue-next#2906 - when a directive is mounted inactive (active: false or no content) then later updated to become active, the updated hook now properly initializes the instance and renders the tooltip/popover. Also adds proper cleanup calls to cleanupDirectiveInstance when removing due to inactive state or missing content, preventing potential memory leaks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/bootstrap-vue-next/src/directives/utils.ts (1)
91-97: Consider edge cases in JSON.stringify-based change detection.Using
JSON.stringifyfor change detection is simple and reliable for most cases. However, it could fail or throw errors if the binding value contains circular references, functions, or non-serializable objects (e.g.,Symbol,BigInt).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/bootstrap-vue-next/src/directives/utils.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2777
File: packages/bootstrap-vue-next/CHANGELOG.md:11-11
Timestamp: 2025-08-18T18:20:08.240Z
Learning: For PR #2777 (BSort updates), keep changes scoped to BTable sorting work; unrelated edits like CHANGELOG typos should be deferred to a separate PR/issue.
📚 Learning: 2025-09-12T14:46:49.416Z
Learnt from: CR
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-12T14:46:49.416Z
Learning: Applies to packages/bootstrap-vue-next/src/components/index.ts : When adding a new component, update packages/bootstrap-vue-next/src/components/index.ts to export it
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-09-12T14:46:49.416Z
Learnt from: CR
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-09-12T14:46:49.416Z
Learning: Applies to packages/bootstrap-vue-next/src/App.vue : Use packages/bootstrap-vue-next/src/App.vue as a local test area for component changes
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-05-28T07:01:55.095Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.095Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-04-28T22:48:46.738Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2425
File: packages/bootstrap-vue-next/src/plugins/popoverController/index.ts:138-140
Timestamp: 2025-04-28T22:48:46.738Z
Learning: In bootstrap-vue-next, `PopoverOrchestratorParam` includes an `id` field of type `ControllerKey` through inheritance from base types. This field is declared in the `BvControllerOptions` interface and propagated through the type hierarchy.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-08-19T23:30:07.062Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('popover') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this prop spreading/inheritance mechanism.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-08-19T23:30:07.062Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('tooltip') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this inheritance mechanism.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-09-30T23:57:21.526Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2866
File: apps/docs/src/docs/components/demo/ModalModel.vue:11-15
Timestamp: 2025-09-30T23:57:21.526Z
Learning: The bootstrap-vue-next documentation project uses unplugin-vue-components to automatically resolve and import Vue components like BModal, BButton, etc. Explicit imports for these components in script sections are not required.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
🧬 Code graph analysis (1)
packages/bootstrap-vue-next/src/directives/utils.ts (3)
packages/bootstrap-vue-next/src/types/ComponentProps.ts (1)
BPopoverProps(1334-1370)packages/bootstrap-vue-next/src/utils/floatingUi.ts (6)
ElementWithPopper(110-127)resolveActiveStatus(35-36)resolveContent(38-83)resolveDirectiveProps(85-108)bind(129-143)unbind(145-166)packages/bootstrap-vue-next/src/utils/keys.ts (1)
defaultsKey(204-205)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
packages/bootstrap-vue-next/src/directives/utils.ts (4)
24-30: Well-designed state structure for UID-namespaced directives.The
DirectiveInstanceStateinterface provides a clean solution for managing per-instance directive state. Thebindingcache enables efficient change detection, and thedestroyingflag prevents race conditions during cleanup.
117-130: Proper use of destroying flag prevents race conditions.Setting
destroying: truebefore deleting the instance state effectively guards against race conditions during concurrent cleanup operations.
233-300: The updated hook logic correctly addresses past review concerns.The implementation now properly handles:
- Cleanup with
cleanupDirectiveInstancewhen content becomes empty or inactive (lines 246, 257)- Instance initialization when transitioning from inactive/no-content to active state (lines 262-277)
- Race condition prevention via the
destroyingflag check (line 284)However, see separate comment about line 279.
302-310: Clean and correct beforeUnmount implementation.The hook follows the proper cleanup sequence: verify instance existence, unbind the directive, and clean up the UID-namespaced state.
Addresses CodeRabbitAI feedback on PR bootstrap-vue-next#2906: - Add null check to getDirectiveUid to prevent runtime crashes - Remove delete binding.oldValue to respect readonly contract - Both changes improve code safety without changing behavior
There was a problem hiding this 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
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/bootstrap-vue-next/src/directives/utils.ts(3 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2777
File: packages/bootstrap-vue-next/CHANGELOG.md:11-11
Timestamp: 2025-08-18T18:20:08.240Z
Learning: For PR #2777 (BSort updates), keep changes scoped to BTable sorting work; unrelated edits like CHANGELOG typos should be deferred to a separate PR/issue.
📚 Learning: 2025-05-28T07:01:55.095Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2716
File: packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue:240-261
Timestamp: 2025-05-28T07:01:55.095Z
Learning: In BTabs component (packages/bootstrap-vue-next/src/components/BTabs/BTabs.vue), the complex initialization logic with updateInitialActiveIndex and updateInitialActiveId flags is necessary for SSR compatibility. Tab initialization cannot be deferred to mounted lifecycle because tabs must be available for server-side rendering of the initial HTML state.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-04-28T22:48:46.738Z
Learnt from: xvaara
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2425
File: packages/bootstrap-vue-next/src/plugins/popoverController/index.ts:138-140
Timestamp: 2025-04-28T22:48:46.738Z
Learning: In bootstrap-vue-next, `PopoverOrchestratorParam` includes an `id` field of type `ControllerKey` through inheritance from base types. This field is declared in the `BvControllerOptions` interface and propagated through the type hierarchy.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-08-19T23:30:07.062Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('popover') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this prop spreading/inheritance mechanism.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-08-19T23:30:07.062Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2762
File: apps/docs/src/docs/components/tooltip.md:90-94
Timestamp: 2025-08-19T23:30:07.062Z
Learning: In bootstrap-vue-next docs, tooltip.data.ts uses popoverSharedProps('tooltip') to define props, which includes the 'manual' prop through the shared props structure from popover-shared.ts. The manual prop is available in BTooltip documentation through this inheritance mechanism.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
📚 Learning: 2025-09-30T23:57:21.526Z
Learnt from: dwgray
Repo: bootstrap-vue-next/bootstrap-vue-next PR: 2866
File: apps/docs/src/docs/components/demo/ModalModel.vue:11-15
Timestamp: 2025-09-30T23:57:21.526Z
Learning: The bootstrap-vue-next documentation project uses unplugin-vue-components to automatically resolve and import Vue components like BModal, BButton, etc. Explicit imports for these components in script sections are not required.
Applied to files:
packages/bootstrap-vue-next/src/directives/utils.ts
🧬 Code graph analysis (1)
packages/bootstrap-vue-next/src/directives/utils.ts (3)
packages/bootstrap-vue-next/src/types/ComponentProps.ts (1)
BPopoverProps(1334-1370)packages/bootstrap-vue-next/src/utils/floatingUi.ts (6)
ElementWithPopper(110-127)resolveActiveStatus(35-36)resolveContent(38-83)resolveDirectiveProps(85-108)bind(129-143)unbind(145-166)packages/bootstrap-vue-next/src/utils/keys.ts (1)
defaultsKey(204-205)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
When unbind() followed immediately by bind(), the microtask cleanup could delete the newly created element reference. Now only delete the reference if it still points to the element we're unmounting. Fixes race condition identified in CodeRabbitAI review where rapid rebinding in the updated() hook would leak DOM nodes and lose the floating UI reference.
|
@xvaara can you please re-approve this - there were a couple of clean-up things from AI codereview that I thought were worth taking care of before merging this. |
xvaara
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a quick test and read-through. LGTM
* upstream/main: (28 commits) fix(BToggle)! Remove redundant attribute cleanup & update docs for accessibility attributes on show/hide components (bootstrap-vue-next#2918) chore: release main (bootstrap-vue-next#2912) fix: allow custom component props in orchestrator create methods with type safety (bootstrap-vue-next#2922) fix(BModal): prevent focus trap error when no tabbable elements exist (bootstrap-vue-next#2864) Update package description for clarity (bootstrap-vue-next#2917) Update project description for clarity and detail test: add event emission tests for BTable and BTableLite (bootstrap-vue-next#2915) fix(BTableLite): Use primary key to persist row-details state when items change (bootstrap-vue-next#2871) chore: release main (bootstrap-vue-next#2896) feat(BTable): add configurable debouncing ci: Add publint to the build process (bootstrap-vue-next#2909) docs(Offcanvas): Parity pass (bootstrap-vue-next#2900) fix(directives): Robustness fixes for directives (bootstrap-vue-next#2906) docs(BFormInput): document @wheel.prevent for disabling mousewheel events (bootstrap-vue-next#2903) fix(typings): Fix paths to `*.d.mts` files (bootstrap-vue-next#2907) feat: add name and form props to BFormRating for form submission (bootstrap-vue-next#2895) docs: refactor docs to avoid duplication and boilerplate code (bootstrap-vue-next#2891) docs(BToast): Parity (bootstrap-vue-next#2887) docs(BModal): fix attribute to hide footer (bootstrap-vue-next#2888) docs(BPlaceholder): Parity pass (bootstrap-vue-next#2886) ...
Describe the PR
I spent some time in GitHub Copilot today exploring whether to keep directives or not. Below is what we came up with along with the rationale.
This code in this PR contains the stability fixes that we landed on as high priority if we intend to support directives moving forward (we probably want them even if we are going to deprecate anytime after our initial official release).
I'm definitely leaning pretty hard on AI for this, so I would appreciate @xvaara and @VividLemon's eyes on it to check for hallucinations. But if this looks reasanbly sane, here's what I'd like to do:
Also note that this PR depends on the internal
$.uid. While undocumented, it is described in vue's typescript and Vuetify uses it in basically the same way. If everything else looks good, but you find the use of$.uidunnaceptable, I believe there is a reasonable way to usegetId()to make this work as well.Executive Summary
Recommendation: KEEP DIRECTIVES with stability improvements
After comprehensive analysis of 77 GitHub issues, codebase review, comparison with Vuetify's approach, and Vue.js best practices, I recommend continuing to support directives while implementing the stability improvements detailed below.
Key Points
✅ DX Benefits Are Real:
v-b-tooltip="text"is significantly cleaner than<BTooltip><template #activator>...</template></BTooltip>✅ Migration Path: Helps users migrate from Bootstrap-Vue v2
✅ Proven Pattern: Vuetify successfully uses similar directive patterns in production
✅ Issues Are Fixable: Most stability problems stem from implementation details, not the directive pattern itself
Issue Analysis
Issues Reviewed: 77 total (9 open, 68 closed)
Critical Issues (Fixed in this PR):
Race Conditions in BToggle (BToggle directive producing console error when unmounted quickly #2856)
dataset.bvtogglebefore each iteration and registrationMemory Leaks in Tooltip/Popover
setTimeout(() => div?.remove(), 0)could fire after component destructionqueueMicrotask()with destruction flag checkTooltip Reactivity (vBTooltip: updating tooltip
titleto an empty string does not update tooltip text #2781, Directive Tooltips disappear when their content is reactive and are updated updated #2454, BTooltip directive not updating content when title changes #2273)updatedhook logic with proper cleanup when content becomes emptyMulti-Instance Collisions (New)
binding.instance!.$.uid)Medium Priority Issues:
Performance with Many Directives (Performance dropped when use many tooltip directives or popovers #2232)
DevTools Pollution (Tooltips and Popovers clutter Vue DevTools with unnamed "App" entries #2700)
Placeholder Span Layout Issues (<span>-element placed by v-b-tooltip directive shifts content in flex container #2225, Ability of adding option to prevent span (_placeholder ) element rendered (tooltip directive/popover) #2228)
<span>elements that affect flexbox layoutLow Priority Issues:
Vue.js Best Practices Assessment
Official Guidance
From Vue.js documentation:
Our Directive Usage
v-b-tooltipv-b-popoverv-b-togglev-b-modalv-b-scrollspyv-b-color-modeVerdict: While v-b-tooltip, v-b-popover, and v-b-toggle technically violate Vue's stated best practice, this is more of a guideline violation than a hard rule. The pattern is defensible given:
Comparison with Vuetify
What Vuetify Does
Vuetify does use directives in similar ways:
Key Differences:
useDirectiveComponenthandles provides/context properlyLessons Learned from Vuetify
✅ Our code already uses the
⚠️ We have more complex logic that needs better protection against race conditions
findProvidespattern from Vuetify✅ We should adopt their UID-based tracking approach
✅ We should document directives as "convenience wrappers"
Stability Issues Found
1. Race Conditions in BToggle (FIXED)
Before:
After:
2. Memory Leaks in Tooltip/Popover (FIXED)
Before:
After:
Benefits:
queueMicrotask()3. Improved Tooltip/Popover Reactivity (FIXED)
Before:
After:
4. Multi-Instance Collisions (FIXED)
Problem:
When multiple component instances used the same directive on the same or different elements, they would overwrite each other's state:
This caused:
destroyingflag causes cross-component contaminationFix:
Implemented UID namespacing pattern from Vuetify, using
binding.instance!.$.uidto isolate state per component instance:Updated TypeScript Types:
Benefits:
Directives Updated:
el.$__tooltip[uid]namespaceel.$__popover[uid]namespaceel.$__scrollspy[uid]namespaceImplementation Quality
Strengths
✅ Already using Vuetify's
findProvidespattern✅ Good test coverage (1643 tests passing)
✅ Proper cleanup hooks (beforeUnmount)
✅ UID-based instance tracking (prevents multi-instance collisions)
✅ JSON-based change detection (per-instance caching for performance)
Weaknesses (Now Fixed)
✅
Race conditions in async registration loops✅
Memory leaks from setTimeout cleanup✅
Incomplete reactivity handling✅
Multi-instance state collisionsRecommendations
Short-term (Completed in this PR)
✅ Fix race conditions - Added proper mount state checks
✅ Fix memory leaks - Switched to queueMicrotask with destruction flags
✅ Improve reactivity - Clean up when content becomes empty/inactive
✅ Implement UID namespacing - Adopted Vuetify's pattern for multi-instance safety
✅ Add code comments - Document the patterns used
Medium-term (Future Work)
Performance Optimization
Documentation
Testing
Long-term (Strategic)
Component-First Approach
Alternative Syntax
Decision Matrix
Score: 5 ✅ vs 2 ❌ for keeping directives
Testing Results
All tests pass with the improvements:
Specific directive test:
Conclusion
KEEP THE DIRECTIVES but with the implemented stability improvements.
Why Keep Them?
Why This Works?
The Vue.js guidance about directives being "only for DOM manipulation" is a best practice, not a hard rule. It's meant to prevent abuse and maintain clarity. In our case:
What Changed?
This PR implements critical stability improvements:
Next Steps
Files Modified
packages/bootstrap-vue-next/src/utils/floatingUi.ts- Improved unbind logic, updated TypeScript interfaces for UID namespacingpackages/bootstrap-vue-next/src/directives/BTooltip/index.ts- Fixed race conditions, reactivity, and implemented UID namespacingpackages/bootstrap-vue-next/src/directives/BPopover/index.ts- Fixed race conditions, reactivity, and implemented UID namespacingpackages/bootstrap-vue-next/src/directives/BScrollspy/index.ts- Implemented UID namespacing for multi-instance safetypackages/bootstrap-vue-next/src/directives/BToggle/index.ts- Fixed async registration race conditionsSmall replication
See bugs referenced above
PR checklist
What kind of change does this PR introduce? (check at least one)
fix(...)feat(...)fix(...)docs(...)The PR fulfills these requirements:
CHANGELOGis generated from these messages, and determines the next version type. Pull requests that do not follow conventional commits or do not have an override will be deniedSummary by CodeRabbit
Bug Fixes
Improvements