Migrating to 0.1.0
0.1.0 is the first release with breaking changes. npm's caret is an exact pin below 1.0.0, so a ^0.0.11 range never resolves to it — you only arrive here deliberately. 0.0.11 stays published, so "do not upgrade" is a valid rollback.
Each section below carries a one-line test for whether it affects you. Start with the table — most upgrades touch one or two rows.
Does this affect me?
| Change | Affects you if | Kind |
|---|---|---|
props is now type-checked | You pass props to useModal() or open() — this is the headline break, and the one most likely to stop a build | Breaking |
slots now render | You pass slots to useModal() | Behavioural |
| Props now reset on close | You mutate a modal's props in place, or relied on merged props surviving a close | Behavioural |
| Props are copied at registration | You mutate the props object you handed to useModal() and expect the modal to follow | Behavioural |
UseModalOptions is a union type | You wrote against UseModalOptions as an interface — declaration merging, implements | Breaking |
Vue ^3.5.0 required | Your package.json resolves vue below 3.5.0 | Breaking |
useModal() is setup-only | You call useModal() outside a component setup() | Breaking |
| The registry was never public | You reached into vue-modal-manager/dist/... for the registry | Breaking |
| Duplicate ids warn | Two useModal() calls share an explicit id | Warning only |
close()/open() after unmount | You hold a close or open reference past the owning component's lifetime | Fix |
openAsync() and close(result) | Nothing — additive | New |
| Per-modal configuration | Nothing — additive | New |
useModalManager() | Nothing — additive | New |
props is now type-checked
Affects you if: you pass props to useModal() or open().
This is the headline break. props was typed ExtractPropTypes<ComponentType>, which is a misuse: ExtractPropTypes expects a props options object such as { title: { type: String } }, not a component type. Applied to typeof NModal it collapses to an empty type, so every props object compiled clean:
// 0.0.11: compiles. Nothing was ever checked.
useModal<typeof NModal>({
component: NModal,
props: { totallyMadeUpProp: 123, anotherFakeOne: 'x' }
})// 0.0.11: compiles. Nothing was ever checked.
useModal<typeof NModal>({
component: NModal,
props: { totallyMadeUpProp: 123, anotherFakeOne: 'x' }
})On 0.1.0 the props type is inferred from the value you pass as component, and checked:
// 0.1.0: no type argument needed, and the fake props are errors.
useModal({
component: NModal,
props: { totallyMadeUpProp: 123 }
// ^ Object literal may only specify known properties
})// 0.1.0: no type argument needed, and the fake props are errors.
useModal({
component: NModal,
props: { totallyMadeUpProp: 123 }
// ^ Object literal may only specify known properties
})The errors are accurate. Every one of them names a prop the component never accepted, so the fix is to correct or remove it. Props stay partial at both call sites, so nothing forces you to supply a complete props object.
An explicit type argument still works, so existing useModal<typeof NModal>({ ... }) call sites do not need editing:
useModal<typeof NModal>({ component: NModal, props: { preset: 'card' } })useModal<typeof NModal>({ component: NModal, props: { preset: 'card' } })The escape hatch
If the errors are numerous and you want the build green while you work through them, as any on the props object turns the check off for that one call site:
useModal({ component: NModal, props: { ...whatever } as any })useModal({ component: NModal, props: { ...whatever } as any })This is a deliberate, supported escape hatch rather than an accident of the type — it is why the unresolved case falls back to a permissive record instead of never. Treat it as a to-do marker: it silences a real finding.
Components whose props cannot be inferred
For a plain object component, or a value typed as bare Component, the props type cannot be destructured and falls back to a permissive record. Those call sites compile exactly as they did on 0.0.11 — a clean build is therefore not proof that your props were checked. If you want the check, give the component a concrete type (defineComponent(...) or a single-file component import) rather than widening it to Component.
class, style, key and ref
The check runs against the component's $props, which carries the standard component attributes, so class, style and the onVnode* hooks still type-check.
key and ref are excluded and now error. Both are vnode concerns rather than props: <ModalProvider> already supplies its own key per registry entry, and a ref would register a template ref against markup you never wrote. If you were passing either, remove it — neither did what it looked like it did.
slots now render
Affects you if: you pass slots to useModal().
slots was accepted, typed any, and stored — and the provider rendered the modal component with no children, so the content silently vanished. It now renders, forwarded to the component as its own slots with no wrapper element:
useModal({
component: NModal,
slots: {
default: () => h('p', 'Are you sure?'),
footer: () => h(NButton, () => 'Close')
}
})useModal({
component: NModal,
slots: {
default: () => h('p', 'Are you sure?'),
footer: () => h(NButton, () => 'Close')
}
})If you worked around the gap by also passing that content through props, you will now render it twice. Remove one of the two.
Top-level props now reset on close
Affects you if: you mutate a modal's props in place, or relied on props merged in through open({ props }) surviving a close.
resetPropsOnClose defaults to true and always has. What changed is that it now works. initialProps was assigned the same object reference as props at registration, so the snapshot was not a snapshot: any in-place mutation of the live props changed the thing reset was supposed to restore. 0.1.0 takes a shallow copy at registration, and restores a fresh copy on close, so the snapshot survives repeated open/close cycles.
Props that previously persisted across a close now reset to their registration values. If you were relying on that persistence, set resetPropsOnClose: false on that modal.
Reset is shallow, deliberately
A nested object inside props is shared with the snapshot and is not restored. Deep cloning is not done because modal props legitimately carry functions, component references, and reactive objects, and structuredClone throws on functions. Pass a fresh nested object through open({ props }) when you need nested values reset.
Props are copied at registration
Affects you if: you keep a reference to the props object you handed to useModal() and mutate it, expecting the modal to follow.
On 0.0.11 the registry stored that object, so it was live in both directions: your later mutations reached the modal, and the library's own prop reset wrote back into your object. 0.1.0 takes a shallow copy instead. That is what makes the reset snapshot an actual snapshot, and it is what lets the documentation promise that options is never written to — but it also means a modal no longer follows the object you passed:
const props = reactive({ title: 'Initial' })
useModal({ component: NModal, props })
props.title = 'Changed' // 0.0.11: the modal followed. 0.1.0: it does not.const props = reactive({ title: 'Initial' })
useModal({ component: NModal, props })
props.title = 'Changed' // 0.0.11: the modal followed. 0.1.0: it does not.Two supported replacements, both unchanged in spirit from what you were doing:
// A ref as a prop *value* stays live. `<ModalProvider>` unwraps it when binding,
// the way a template would, so writing `.value` re-renders the modal.
const title = ref('Initial')
const { open } = useModal({ component: NModal, props: { title } })
title.value = 'Changed'
// Or merge the new values in at the moment you open.
open({ props: { title: 'Changed' } })// A ref as a prop *value* stays live. `<ModalProvider>` unwraps it when binding,
// the way a template would, so writing `.value` re-renders the modal.
const title = ref('Initial')
const { open } = useModal({ component: NModal, props: { title } })
title.value = 'Changed'
// Or merge the new values in at the moment you open.
open({ props: { title: 'Changed' } })A ref is unwrapped one level deep, at the top of props. A ref nested inside a plain object in props reaches the component as a ref.
UseModalOptions is now a union type
Affects you if: you wrote against UseModalOptions as an interface — declaration merging, or implements.
It is now a type: an intersection of the base options with a union of the three configuration shapes. That union is what makes half an explicit openPropName / openEventName pair a type error, which a plain interface cannot express.
Uses that only reference the type — const options: UseModalOptions<typeof NModal> = ..., or a function parameter — are unaffected.
Vue ^3.5.0 is now required
Affects you if: your package.json resolves vue below 3.5.0.
The peer range moved from ^3.3.0 to ^3.5.0. Auto-generated modal ids now come from Vue's useId(), which was added in 3.5 and is the only id primitive that agrees between a server render and the client render of the same component. Without it, an auto-generated id differs across the hydration boundary.
npm install vue@^3.5.0npm install vue@^3.5.0If you cannot move off Vue 3.3 or 3.4, stay on 0.0.11.
useModal() must be called inside a component setup()
Affects you if: you call useModal() anywhere other than a component's setup() — module scope, a Pinia store action, a router guard, a plain helper function.
The modal registry is no longer a module-level singleton. It is created by app.use(VueModalManager, ...) and reached with inject(), which only works during setup(). Calls from anywhere else now throw instead of half-working:
Missing modal registry. `useModal()`, `useModalManager()` and `<ModalProvider>` must be
called from a component setup in an app that installed VueModalManager. Please refer to
the documentation on how to setup Vue modal manager: https://vue-modal-manager.netlify.appMissing modal registry. `useModal()`, `useModalManager()` and `<ModalProvider>` must be
called from a component setup in an app that installed VueModalManager. Please refer to
the documentation on how to setup Vue modal manager: https://vue-modal-manager.netlify.appBefore — a modal handle built in module scope and imported wherever it was needed:
import { useModal } from 'vue-modal-manager'
import UserCreateModal from '@/components/UserCreateModal.vue'
// Throws on 0.1.0: there is no setup context here.
export const userCreateModal = useModal({
id: 'user-create-modal',
component: UserCreateModal
})import { useModal } from 'vue-modal-manager'
import UserCreateModal from '@/components/UserCreateModal.vue'
// Throws on 0.1.0: there is no setup context here.
export const userCreateModal = useModal({
id: 'user-create-modal',
component: UserCreateModal
})After — the call moves into the component, and the shared id is what ties call sites together:
<script setup>
import { useModal } from 'vue-modal-manager'
import UserCreateModal from '@/components/UserCreateModal.vue'
const { open } = useModal({
id: 'user-create-modal',
component: UserCreateModal
})
</script>
<template>
<button @click="open">Create user</button>
</template><script setup>
import { useModal } from 'vue-modal-manager'
import UserCreateModal from '@/components/UserCreateModal.vue'
const { open } = useModal({
id: 'user-create-modal',
component: UserCreateModal
})
</script>
<template>
<button @click="open">Create user</button>
</template>The requirement is not new in spirit: useModal() has always registered an onBeforeUnmount hook, so a call outside setup() already left an entry that was never cleaned up. What changes is that it now fails loudly.
app.use(VueModalManager, ...) itself needs no change — the same call now also creates the registry.
The same applies to <ModalProvider> and useModalManager()
Both reach the registry the same way and throw the same error, so a missing app.use(VueModalManager, ...) is now reported wherever it is first observed rather than only by the provider.
The registry was never a public export
Affects you if: you imported anything other than useModal, useModalManager, ModalProvider, VueModalManager, and the exported types — for instance by reaching into vue-modal-manager/dist/... for the modals object.
src/lib/store.ts was never re-exported from the package entry point, so the registry was already unreachable through any supported import. It is now app-scoped as well, and there is no supported path forward for code that depended on getting at it. The nearest supported replacements are useModalManager().closeAll() for global operations and a shared explicit id for reaching one modal from several call sites.
Duplicate explicit ids now warn
Affects you if: two useModal() calls in your app pass the same explicit id.
Nothing about the behaviour changed — the later registration still replaces the earlier one, and both callers still drive a single modal. In development you now get a console.warn naming the duplicated id, because the accidental version of this is otherwise silent. Sharing an id deliberately is still supported; the warning is not an error and production builds strip it.
close() and open() after unmount are now no-ops
Affects you if: you hold on to a close or open reference past the owning component's lifetime.
close() used to throw a TypeError, because it wrote to the registry entry before checking that the entry still existed. It now returns without doing anything.
open() already did nothing to the registry in this situation, but it still called your onOpen hook — reporting that a modal had opened when none had. It no longer does. This is the same rule the server-render path follows: onOpen fires only when a modal actually opened.
New: openAsync() and close(result)
Not a break — open() still returns void and close() still takes no argument. openAsync() opens the modal the same way and returns a promise that resolves when the modal closes, with whatever you passed to close(result):
<script setup>
const { openAsync, close } = useModal({
component: ConfirmDialog,
props: { onConfirm: () => close(true), onCancel: () => close(false) }
})
const confirmed = await openAsync()
</script><script setup>
const { openAsync, close } = useModal({
component: ConfirmDialog,
props: { onConfirm: () => close(true), onCancel: () => close(false) }
})
const confirmed = await openAsync()
</script>The promise never rejects and never stays pending once the modal can no longer be closed — dismissal, close-all, unmount and server rendering all resolve it with undefined. See the settlement table.
open() was deliberately left synchronous so that existing fire-and-forget call sites are not flagged by @typescript-eslint/no-floating-promises.
New: per-modal prop and event names
Not a break — the options you pass to app.use(VueModalManager, ...) keep working and are now the application default. Any modal may override them with its own preset, or its own openPropName + openEventName pair, so several UI kits can coexist in one application. Plugin options are now optional for an app whose every modal configures itself.
New: useModalManager()
Not a break — closeAllModals on the useModal() return value keeps working unchanged and is now a documented alias. useModalManager() gives you closeAll without needing a per-modal handle:
<script setup>
import { useModalManager } from 'vue-modal-manager'
const { closeAll } = useModalManager()
</script><script setup>
import { useModalManager } from 'vue-modal-manager'
const { closeAll } = useModalManager()
</script>