Vue Side Effect In A Computed Getter
ID |
javascript.vue_side_effect_in_computed |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Reliability |
Language |
JavaScript |
Tags |
reliability, side-effects, vue |
Description
Reports a write to reactive state inside a Vue computed getter, whichever API the component is written in.
Both spellings of reactive state are recognised: x.value of the composition API and this.x of the options API. A plain local variable assigned inside the getter is not a side effect on the component and is not reported. A setter is left alone — computed({get, set}) and the options {get, set} pair exist so that a computed property can be written to.
Rationale
import { computed, defineComponent, ref } from "vue";
const count = ref(0);
const seen = ref(0);
// Bad — composition API
const doubled = computed(() => {
seen.value = seen.value + 1;
return count.value * 2;
});
// Bad — options API
export default defineComponent({
computed: {
grandTotal() {
this.total = this.rows.length;
return this.total;
}
}
});
-
A computed getter is meant to derive a value and nothing else, and Vue relies on that: the result is cached until one of the dependencies the getter read changes, and Vue decides when — and whether — to run it again.
-
A getter that writes state performs its write an unpredictable number of times: none while the cache holds, once per re-evaluation after that. A counter incremented in a getter counts re-evaluations, not events.
-
When the write touches something the getter depends on, the getter invalidates its own cache: Vue re-runs it, the write fires again, and the effect loops until Vue stops it with "Maximum recursive updates exceeded" — at a stack that names the scheduler rather than the getter.
Remediation
Keep the getter pure. Put the write in a watcher, which exists to react to a change, or in a method called from the event that should cause it.
import { computed, ref, watch } from "vue";
const count = ref(0);
const seen = ref(0);
// Good — the getter only derives
const doubled = computed(() => count.value * 2);
watch(count, () => {
seen.value = seen.value + 1;
});
import { defineComponent } from "vue";
// Good — options API, the write belongs to the watcher
export default defineComponent({
computed: {
grandTotal() {
return this.rows.length;
}
},
watch: {
rows() {
this.touched = true;
}
}
});