Introduction
Vue's watch option with immediate: true executes the watcher callback as soon as the component is created, before the template is rendered. While this is useful for initial data fetching, it can cause problems when combined with other lifecycle hooks that also trigger the same logic:
watch: {
userId: {
handler(newId) {
this.fetchUser(newId);
},
immediate: true, // Fires on mount
}
},
mounted() {
this.fetchUser(this.userId); // Fires again - duplicate call!
}Symptoms
- API endpoint called twice on component mount
- Race conditions between the watcher and mounted hook
- Loading state flickers as two concurrent requests compete
- Data from the first request is overwritten by the second
- Console shows duplicate network requests in the Network tab
Common Causes
- Both
watch: { immediate: true }andmounted()call the same function - Watcher fires before the component is fully initialized
- Parent component passes a prop that changes during the child's initialization
- Multiple watchers on related data all firing on mount
- Vue 3 Composition API
watchwithimmediate: truecombined withonMounted
Step-by-Step Fix
- 1.Remove the duplicate call from mounted when using
immediate: true: - 2.```javascript
- 3.export default {
- 4.props: ['userId'],
- 5.data() {
- 6.return { user: null, loading: false };
- 7.},
- 8.watch: {
- 9.userId: {
- 10.handler(newId) {
- 11.if (newId) {
- 12.this.fetchUser(newId);
- 13.}
- 14.},
- 15.immediate: true,
- 16.},
- 17.},
- 18.methods: {
- 19.async fetchUser(id) {
- 20.this.loading = true;
- 21.try {
- 22.this.user = await api.getUser(id);
- 23.} finally {
- 24.this.loading = false;
- 25.}
- 26.},
- 27.},
- 28.};
- 29.
` - 30.Use a guard flag to prevent the watcher from firing on the initial mount:
- 31.```javascript
- 32.export default {
- 33.data() {
- 34.return { _isInitialized: false };
- 35.},
- 36.watch: {
- 37.someValue: {
- 38.handler(newVal) {
- 39.if (!this._isInitialized) return;
- 40.this.handleValueChange(newVal);
- 41.},
- 42.},
- 43.},
- 44.mounted() {
- 45.this._isInitialized = true;
- 46.// Now the watcher will fire for subsequent changes only
- 47.},
- 48.};
- 49.
` - 50.In Vue 3 Composition API, use
watchEffector separate watchers: - 51.```javascript
- 52.import { ref, watch, onMounted } from 'vue';
export default { setup(props) { const user = ref(null);
// Initial fetch in onMounted onMounted(async () => { user.value = await api.getUser(props.userId); });
// Subsequent changes handled by watch (no immediate: true) watch(() => props.userId, async (newId) => { user.value = await api.getUser(newId); });
return { user }; }, }; ```
- 1.Cancel in-flight requests when the watcher fires again:
- 2.```javascript
- 3.watch: {
- 4.userId: {
- 5.async handler(newId) {
- 6.if (this._abortController) {
- 7.this._abortController.abort();
- 8.}
- 9.this._abortController = new AbortController();
try { this.user = await api.getUser(newId, { signal: this._abortController.signal, }); } catch (e) { if (e.name !== 'AbortError') throw e; } }, immediate: true, }, }, ```
Prevention
- Choose one initialization pattern: either
watch: { immediate: true }ORonMounted, not both for the same logic - Document which data fetching strategy each component uses
- Use AbortController to cancel superseded requests when watchers fire rapidly
- In Vue 3, prefer
watchEffectfor side effects that depend on reactive state - Test component mount behavior with Vue Test Utils to verify single API call
- Add request deduplication at the API layer to prevent duplicate network calls
- Use a loading state that prevents concurrent requests for the same resource