Why is this an issue?

onUpdated() (Composition API) and the updated() lifecycle hook (Options API) run after Vue re-renders a component in response to a reactive state change. Mutating a ref’s `.value or a reactive() object’s property directly inside that hook changes reactive state again, which can trigger another re-render, which runs the hook again, and so on: an infinite update loop.

Vue’s own documentation warns against this directly: "Do not mutate component state in the updated hook - this will likely lead to an infinite update loop!"

This rule flags a direct, unconditional mutation of reactive state inside onUpdated()/updated() when the assigned value is guaranteed to differ from the previous one, for example an increment or decrement (+`/`--`), a compound assignment (`= and similar), an assignment that reads the same binding it mutates, or a call to a non-deterministic function such as Date.now(), Math.random(), or performance.now(). A mutation guarded by an if, a ternary, or an early return is not flagged, since the concern is specifically about mutations that run unconditionally on every call. An unconditional assignment of a literal or otherwise-stable value, such as count.value = 3, is also not flagged: Vue’s reactivity does not re-trigger when the newly assigned value equals the previous one, so it converges after a single call.

In development, Vue’s scheduler catches this: it counts how many times the same reactive effect re-schedules itself and throws once that count passes 100, reporting a message starting with "Maximum recursive updates exceeded. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself.". That safety net is stripped out of production builds, so the exact same code that fails loudly in development instead runs unbounded in production, freezing the page.

To fix this, guard the mutation with a condition that becomes false once the update converges. If the real intent is to derive one piece of state from another rather than run a side effect after every render, move the logic out of onUpdated/updated entirely, for example into a watch or a computed.

Code examples

Noncompliant code example

<script setup>
import { ref, onUpdated } from 'vue';

const searchCount = ref(0);

onUpdated(() => {
  searchCount.value++; // Noncompliant: unconditionally mutating reactive state here can trigger an infinite update loop
});
</script>

Compliant solution

<script setup>
import { ref, watch } from 'vue';

const query = ref('');
const searchCount = ref(0);

watch(query, () => {
  searchCount.value++;
});
</script>

Noncompliant code example

<script setup>
import { ref, onUpdated } from 'vue';

const retryDelay = ref(1000);

onUpdated(() => {
  retryDelay.value *= 2; // Noncompliant: unconditionally mutating reactive state here can trigger an infinite update loop
});
</script>

Compliant solution

<script setup>
import { ref, onUpdated } from 'vue';

const retryDelay = ref(1000);
const maxRetryDelay = 30000;

onUpdated(() => {
  if (retryDelay.value < maxRetryDelay) {
    retryDelay.value *= 2; // converges once retryDelay reaches maxRetryDelay
  }
});
</script>

Resources

Documentation