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;
    }
  }
});

© 2026 Xygeni. All rights reserved