Avoid Spaghetti Code in Vue 3: When to Use watch vs computed
This article analyzes common misuse of Vue 3's watch for handling both synchronous and asynchronous updates, demonstrates how excessive watchers create spaghetti code, and shows how refactoring with computed can streamline logic, improve maintainability, and help new team members onboard faster.
Case Analysis
In Vue 3, watch is a powerful tool for monitoring reactive data changes and handling side effects, but unreasonable usage can lead to maintenance problems. Below is an example that illustrates such misuse.
<code><template>
{{ dataList }}
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
const dataList = ref([]);
const props = defineProps(["disableList", "type", "id"]);
watch(
() => props.disableList,
() => {
// Complex logic based on disableList, synchronously computes a new list
const newList = getListFromDisabledList(dataList.value);
dataList.value = newList;
},
{ deep: true }
);
watch(
() => props.type,
() => {
// Complex logic based on type, synchronously computes a new list
const newList = getListFromType(dataList.value);
dataList.value = newList;
}
);
watch(
() => props.id,
() => {
// Fetch data from the database
fetchDataList();
},
{ immediate: true }
);
</script></code>The template renders dataList . Updating props.id (including initialization) triggers an asynchronous fetch, while changes to props.disableList and props.type synchronously recompute dataList .
At first glance the code seems fine, but new teammates unfamiliar with the domain quickly encounter problems. The core variable dataList has multiple sources, making the logic hard to follow.
Instead of constantly adding new watchers, we can consolidate synchronous updates using computed and keep only asynchronous updates in watch . This reduces spaghetti code and clarifies the data flow.
Solution
Refactor the component as follows:
<code><template>
{{ renderDataList }}
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
const props = defineProps(["disableList", "type", "id"]);
const dataList = ref([]);
const renderDataList = computed(() => {
const newDataList = getListFromDisabledList(dataList.value);
return getListFromType(newDataList);
});
watch(
() => props.id,
() => {
fetchDataList();
},
{ immediate: true }
);
</script></code>Now the template renders renderDataList , which combines the previously separate synchronous computations into a single computed property.
When new team members receive product requirements related to dataList , the linearized logic makes it easy to understand and modify either the synchronous or asynchronous parts.
For example, updating the synchronous logic can be done by adjusting the computed property:
<code>const renderDataList = computed(() => {
// Add latest processing logic
const xxxList = getListFromXxx(dataList.value);
const newDataList = getListFromDisabledList(xxxList);
return getListFromType(newDataList);
});</code>Summary
Misusing watch leads to hard‑to‑maintain code : When watch handles multiple complex synchronous and asynchronous updates, the component becomes tangled and difficult to understand.
Proper use of computed and watch improves code quality : Moving synchronous logic into computed linearizes business logic, while keeping asynchronous updates in watch separates concerns.
Optimized structure helps new members onboard quickly : Consolidating synchronous logic in computed lets newcomers locate and modify the relevant code faster, boosting development efficiency.
Code logic should be linear and traceable : A clear, thread‑like flow prevents confusing branching and nesting, supporting long‑term maintainability.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.