Component update is skipped when object v-bind changes keys and the new prop value is undefined
Vue version
3.5.38
Link to minimal reproduction
Steps to reproduce
- Open the reproduction.
- Notice that both child components initially display "Add at least one item".
- Click "Add item".
- The click handler adds an item and schedules the counter update using
nextTick(). - Wait for the updates to complete.
- Observe that
itemscontains one item,hasErrorsisfalse, andcounteris1. - Compare the results of the "Dynamic object shape" and "Stable object shape" components.
What is expected?
Both child components should clear the previous errorMessage prop and display:
Count: 1When the object passed through v-bind changes from:
{
errorMessage: 'Add at least one item',
}to:
{
counter: undefined,
}the removal of errorMessage should be detected even though the new prop value is undefined.
The watcher for props.errorMessage should be triggered with the previous error message and undefined as the new value.
What is actually happening?
The component using the stable-shaped binding works correctly: it clears errorMessage and displays:
Count: 1The component using the dynamically shaped binding receives the updated counter but retains the stale errorMessage.
Its effective props become:
{
errorMessage: 'Add at least one item',
counter: 1,
}As a result, it continues to display:
Add at least one itemThe computed binding in the parent is recalculated correctly, but the watcher for props.errorMessage in the child is not triggered because the stale prop is never cleared.
System Info
Reproduces in the Vue SFC Playground using Chrome.Any additional comments?
The reproduction uses nextTick() to intentionally split the state change into two render cycles:
const addItem = () => {
items.value.push('Item');
nextTick(() => counter.value = items.value.length);
};This produces the following props transitions:
// Initial render
{
errorMessage: 'Add at least one item',
}
// First render after adding an item
{
counter: undefined,
}
// Second render after nextTick()
{
counter: 1,
}During the first update, the previous and next props objects contain the same number of keys:
const prevProps = {
errorMessage: 'Add at least one item',
};
const nextProps = {
counter: undefined,
};
Object.keys(prevProps).length === Object.keys(nextProps).length;
// trueWhen Vue checks the counter key from nextProps, the comparison is effectively:
nextProps.counter === prevProps.counter;
// undefined === undefined
// truecounter is not present in prevProps, but reading a missing property also returns undefined. Because the comparison checks the values without checking property presence, Vue considers the props unchanged
and skips the child component update.
The child component therefore retains:
{
errorMessage: 'Add at least one item',
}However, after the skipped update, the component VNode has already advanced to:
{
counter: undefined,
}When counter is changed to 1 after nextTick(), Vue detects that update and patches the child. At this point, the previous VNode no longer contains errorMessage, so the stale errorMessage stored in
the component instance is not cleared.
The child consequently ends up with both the stale error and the new counter:
{
errorMessage: 'Add at least one item',
counter: 1,
}The stable-shaped version avoids the issue because Vue can directly detect the errorMessage value change:
const stableBind = computed(() => ({
errorMessage: hasErrors.value
? 'Add at least one item'
: undefined,
counter: counter.value,
}));A possible fix may be to account for property presence when comparing props, in addition to comparing their values:
!hasOwn(prevProps, key) ||
hasPropValueChanged(nextProps, prevProps, key)Object-style v-bind is documented as equivalent to passing the object properties individually. The documentation does not state that the bound object must retain the same set of keys:
https://vuejs.org/guide/components/props.html#binding-multiple-properties-using-an-object
Source: vuejs/core