Vue.js Stack Overflow Answers
Selected technical solutions from my Stack Overflow history. These are curated for depth — reactivity, rendering, slots, and component architecture — not a vote leaderboard.
Problem
A loading flag was a ref in the component. On each request the code replaced it with a new ref returned from the API layer. The first load updated the template; later clicks left the UI stuck on false.
Solution
Do not reassign refs. Declare loading with const and mutate loading.value, or keep a stable ref (for example a promiseRef helper) that flips itself when a promise starts and settles. The template must keep the same reactive object it subscribed to.
Why it works
Vue tracks the original ref object. loading = ref(newValue) leaves the template bound to the previous object, so later .value writes go nowhere visible. const loading = ref(false) plus loading.value = … keeps that identity.
Code
const loading = ref(false);
async function loadEvents() {
loading.value = true;
const results = await backendApi.getEvents(selectedDate.value);
loading.value = false;
}
3 score · Accepted · 4.8K question views✓
View on Stack Overflow ↗Problem
Child continents were stored as a plain object of refs. Text interpolation showed the active flag, but v-if never flipped the child components.
Solution
That pattern hits Vue's template-unwrap caveat: nested refs unwrap at the end of an interpolation, not inside v-if. Either write continents.asia.value, wrap the map in reactive(), or drop the object of refs and switch on a single continent string.
Why it works
v-if sees a Ref object (always truthy). {{ continents.asia }} unwraps because the ref is the last value of the expression. A string ref avoids the nested-ref trap entirely.
Code
const continent = ref('africa');
<AsiaCountries v-if="continent === 'asia'" />
<AfricaCountries v-if="continent === 'africa'" />
3 score · Accepted✓
View on Stack Overflow ↗Problem
A child copied a destructured prop into ref({ companyId }) and later saw undefined / stale data when the parent updated the prop.
Solution
Destructuring props snapshots the raw value. Keep the link with computed(() => props.companyId) (or toRef) instead of assigning the destructured field into a new object.
Why it works
In Vue 3.5+ destructuring compiles to __props.companyId, which is reactive only while you read it in a reactive context. Assigning that read into a plain object stores the current primitive and drops tracking. In 3.4- the destructured binding is already a raw value.
Code
const companyId = computed(() => props.companyId);
2 score · Accepted✓
View on Stack Overflow ↗Problem
A wrapper around a date picker needed to speak Date internally while the parent stored dayjs (or similar) via v-model.
Solution
Use a computed with get/set as the inner v-model: convert on read, emit the converted value on write. Prefer the modelValue prop (or defineModel with get/set from Vue 3.4) so the wrapper itself can use v-model.
Why it works
computed get/set is the supported way to adapt a v-model without mutating props. The child writes to the computed; the setter emits update:modelValue, so the parent remains the source of truth.
Code
const value = computed({
get() {
return dayjs(props.value).toDate();
},
set(val) {
emit('update:value', dayjs(val));
}
});
1 score · Accepted✓
View on Stack Overflow ↗Problem
VeeValidate's Field exposes field through a scoped slot. The parent needed that object in script (watchers, appending values) and had no template-ref into the slot scope.
Solution
Render a tiny functional component inside the slot that receives field as a prop and writes it into a parent ref. The slot stays declarative; script gets a live handle to the same object.
Why it works
Scoped-slot props exist only in the slot vnode tree. A functional child is just another vnode: v-bind="{ field }" copies the slot prop onto the fake component, which can assign it into ordinary setup state.
Code
const field = ref();
const proxy = props => {
field.value = props.field;
};
<Comp #="{field}"><proxy v-bind="{field}"/></Comp>
1 score · Accepted · 192 question views✓
View on Stack Overflow ↗Problem
The page was server-rendered HTML with custom tags such as <hello-world :msg="…">. There was no #app root, and createApp({}).mount() did not hydrate those islands.
Solution
createVNode + render can mount any SFC onto an existing element while sharing appContext from a hidden Vue app. Pair that with globalThis._importMeta_.glob to find every .vue file, map filenames to tag names, copy :attrs into props, then replace the placeholder node with the rendered DOM.
Why it works
render(vnode, elem) is the same primitive mount() uses, without requiring one tree under a single root. Copying app._context keeps plugins/components available to each island.
Code
function mountComponent(app, elem, component, props) {
const vNode = createVNode(component, props);
vNode.appContext = app._context;
render(vNode, elem);
return vNode.component;
}
5 score · Accepted · 2.7K question views✓
View on Stack Overflow ↗Problem
A container needed to render a caller-supplied component with caller-supplied props, rather than hard-coding the child.
Solution
Pass the component option (or imported SFC) as a prop and render it with Vue's <component :is> plus v-bind for the extra props.
Why it works
:is accepts a component definition, not only a string tag. v-bind spreads the prop object onto that dynamic vnode.
Code
<component :is="component" v-bind="componentProps" />
3 score · Accepted✓
View on Stack Overflow ↗Problem
A confirm-on-click directive added a capture listener. The author asked whether beforeUnmount cleanup was required, and whether a directive was the right tool.
Solution
An unreferenced listener on a removed node is garbage-collected. Directives are still the wrong fit: Vue is VDOM-centric. Wrap default-slot vnodes, intercept click with a <dialog>, then replay the original click after confirm.
Why it works
mergeProps({ onClick }, vnode.props) installs a Vue listener on cloned slot vnodes. preventDefault + showModal stops navigation; after Yes, a deferred target.click() runs the original handler once.
Code
const render = () => {
return $slots.default().map(vnode => (
vnode.props = mergeProps({ onClick }, vnode.props ?? {}),
vnode
));
};
3 score · Accepted · 360 question views✓
View on Stack Overflow ↗Problem
Quasar q-btn-toggle expected named scoped slots. The template put v-if/v-for on <div>s inside the default slot, so the named slots never registered and Vue reported a missing node.
Solution
Named slots must be <template #name> children of the component, not nested DOM. Use one <template v-for> with a dynamic slot name, and fold the length check into the v-for source.
Why it works
Slot compilation looks at the component's direct children. A wrapping <div> is default-slot content, so #[type.value] inside it never becomes a named slot.
Code
<template
v-for="type in options.length > 1 ? options : []"
:key="type.value"
#[type.value]
>
<q-icon v-if="props[type.value] && encryptionType === type.value" name="check_circle" />
</template>
2 score · Accepted✓
View on Stack Overflow ↗Problem
Template refs of a v-for list sat in an array ref. watch(entryComponentRefs) did not see items being added, and watching .value fired twice.
Solution
A ref's default watch is shallow (the .value identity). Deep-watch the inner array, or — better — watch the source data with { flush: 'post' } and read the updated DOM refs in that callback. Driving logic from data, not from DOM identity, avoids the double-flush.
Why it works
Vue's doWatch only sets deep = true automatically for reactive() objects, not for isRef(source). flush: 'post' runs after the renderer patches, so entryComponentRefs already matches the new list. An extra job queued while triggering the deep array watch is what caused the duplicate callback.
Code
watch(entries, () => {
// entryComponentRefs is up to date here
}, { flush: 'post' });
2 score · Accepted✓
View on Stack Overflow ↗Problem
A v-hover directive needed to write back into a ref. In the template, refs are unwrapped, so the directive received a boolean, not the ref, and could not assign hover.value.
Solution
Either pass a reactive({ hover }) bag so the directive mutates value.hover, or skip the directive: a functional component can attach mouseenter/mouseleave on slot vnodes and emit update:modelValue.
Why it works
Directive bindings are evaluated in the template unwrap context. A reactive object is passed by identity, so nested .hover stays a writable field. The functional approach uses VDOM listeners instead of fighting unwrap.
Code
const Hover = (_, { slots, emit }) => {
const out = slots.default();
out.forEach(vnode => Object.assign(vnode.props, {
onMouseenter() { emit('update:modelValue', true); },
onMouseleave() { emit('update:modelValue', false); }
}));
return out;
};
2 score · Accepted✓
View on Stack Overflow ↗Problem
The caller needed a Vue component's HTML string immediately (for example to embed elsewhere), not as a live subtree.
Solution
Create a detached div, h(Component) with the current appContext, render() into that div, read innerHTML, then remove the node. To keep the string in sync without listing dependencies, observe the live subtree with MutationObserver.
Why it works
render() runs the same compiler output as mount. Sharing appContext is required so the vnode can resolve components registered on the app. MutationObserver then sees VDOM patches as real DOM mutations.
Code
const div = document.createElement('div');
const vNode = h(Bubble);
vNode.appContext = self.appContext;
renderVue(vNode, div);
const html = div.innerHTML;
div.remove();
2 score · Accepted✓
View on Stack Overflow ↗Problem
A useModal composable tried to return Teleport/h(BaseModal) from a function. Nothing appeared: the render function returned no mounted tree, and there was no parent to host the vnode.
Solution
A composable is not a renderer. Build the modal vnode, mount it manually (and unmount on close), and return the component's exposed API so callers can call show/close.
Why it works
h() only creates a vnode. Without render/mount, Teleport never runs. Manual mount gives the modal its own host node; unmount avoids leaking the overlay after close.
Code
const { show } = useModal();
const modal = ref();
const content = { default: () => h(Foobar) };
modal.value = show({ title: 'Foobar' }, content);
2 score · Accepted · 654 question views✓
View on Stack Overflow ↗Problem
A child mutated fields on an object v-model. @update:modelValue never ran, unlike the docs' primitive counter example.
Solution
v-model on a ref tracks replacement of .value, not deep mutation. Emit a new object ({ ...model.value, age: … }) or watch the object and skip v-model. Vue documents object props as readonly but does not deeply freeze them.
Why it works
The parent ref's trigger runs when its .value identity changes. Mutating age on the same object never changes that identity, so update:modelValue is not emitted. Cloning trades nested-reference stability for a detectable assignment.
Code
function update() {
model.value = { ...model.value, age: model.value.age + 1 };
}
2 score · Accepted · 1.5K question views✓
View on Stack Overflow ↗Problem
Slot inner HTML arrived as a string containing v-model="myValue". A declarative slot worked; a render function had no compiler to bind myValue on the parent.
Solution
compile() the HTML to a render function, then call it with a render context object that contains myValue. The compiled function uses with(_ctx), so _ctx.myValue is the same ref the parent owns.
Why it works
The compiler emits onUpdate:modelValue: $event => (myValue = $event) against the render context, not against setup locals. Providing { myValue } as _ctx is what makes the assignment hit the parent's ref.
Code
return function render(_ctx, _cache) {
with (_ctx) {
return withDirectives(createElementVNode('input', {
'onUpdate:modelValue': $event => (myValue = $event)
}), [[vModelText, myValue]]);
}
}
1 score · Accepted · 395 question views✓
View on Stack Overflow ↗Problem
The asker wanted to spawn MyItem instances by calling h() on a button click and park those vnodes on the page.
Solution
Don't accumulate vnode instances. Push data into a list and let v-for create the components. h() is for render functions, not for an ad-hoc instance factory.
Why it works
Vue owns vnode identity across patches. A homemade array of h() results is not registered in the component tree the way v-for children are, so updates, keys, and unmount miss them.
Code
const items = reactive([]);
function generateItem() {
items.push('Item ' + counter++);
}
<my-item v-for="item in items">{{ item }}</my-item>
1 score · Accepted✓
View on Stack Overflow ↗Problem
The parent dynamically added child forms (key, type, required) and needed the filled-in objects back, not a bag of template refs.
Solution
Keep an items array on the parent and v-model each ChildComponent to items[index]. The child writes into a computed get/set (or defineModel) so each row is just an entry in that array.
Why it works
v-model on a list index is a two-way bind to the parent's data. The parent already has the source of truth; it never needs to reach into child internals.
Code
<ChildComponent v-for="(_, index) in items" :key="index" v-model="items[index]" />
3 score · Accepted · 3.3K question views✓
View on Stack Overflow ↗Problem
A scoped slot exposed prop, but the parent needed that value in script (computed, headings), not only in the slot template.
Solution
Lift the state: expose via defineExpose + template ref, assign during slot render (slotProp = prop), or share a ref the child writes. The slot expression is still the child's prop; script reads the lifted copy.
Why it works
Scoped-slot props are compiled into the slot function's arguments. They are not automatically setup bindings on the parent. Copying or exposing them creates a reactive source the parent can compute from.
Code
<comp ref="$comp" #="{prop}">{{ (slotProp = prop, '') }}</comp>
<h1>{{ $comp?.prop }}</h1>
1 score · Accepted✓
View on Stack Overflow ↗Problem
A parent switched defineAsyncComponent children and called an exposed method from onMounted. The inner component was often still loading, so the ref was null.
Solution
Queue calls in a reactive array. Watch both the queue and the async component ref; when the instance exists, drain the queue onto the exposed method.
Why it works
defineAsyncComponent resolves after mount. A watch on [calls, componentRef] is the handshake: data can arrive early, the instance late, and nothing is dropped.
Code
watch([methodACalls, currentComponentRef], () => {
if (!currentComponentRef.value) return;
while (methodACalls.length) {
currentComponentRef.value.methodA(...methodACalls.shift());
}
});
1 score · Accepted · 168 question views✓
View on Stack Overflow ↗Problem
A JSON-driven renderer instantiated arbitrary SFCs. If setup() threw, the whole tree died instead of showing a fallback.
Solution
A wrapper functional component copies the target, replaces setup with try/catch, and on failure returns a render function that h()s an Error component. Original component objects stay untouched.
Why it works
onErrorCaptured does not catch synchronous setup() throws in all cases this library needed. Wrapping setup intercepts the error at the same call Vue uses to create the instance, before render.
Code
component = {
...component,
setup(props, extra) {
try {
return _setup.call(component, props, extra ?? {});
} catch (e) {
return () => h(Error, { message: e.message });
}
}
};
1 score · Accepted · 1.7K question views✓
View on Stack Overflow ↗ View All Answers ↗