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:

javascript
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 } and mounted() 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 watch with immediate: true combined with onMounted

Step-by-Step Fix

  1. 1.Remove the duplicate call from mounted when using immediate: true:
  2. 2.```javascript
  3. 3.export default {
  4. 4.props: ['userId'],
  5. 5.data() {
  6. 6.return { user: null, loading: false };
  7. 7.},
  8. 8.watch: {
  9. 9.userId: {
  10. 10.handler(newId) {
  11. 11.if (newId) {
  12. 12.this.fetchUser(newId);
  13. 13.}
  14. 14.},
  15. 15.immediate: true,
  16. 16.},
  17. 17.},
  18. 18.methods: {
  19. 19.async fetchUser(id) {
  20. 20.this.loading = true;
  21. 21.try {
  22. 22.this.user = await api.getUser(id);
  23. 23.} finally {
  24. 24.this.loading = false;
  25. 25.}
  26. 26.},
  27. 27.},
  28. 28.};
  29. 29.`
  30. 30.Use a guard flag to prevent the watcher from firing on the initial mount:
  31. 31.```javascript
  32. 32.export default {
  33. 33.data() {
  34. 34.return { _isInitialized: false };
  35. 35.},
  36. 36.watch: {
  37. 37.someValue: {
  38. 38.handler(newVal) {
  39. 39.if (!this._isInitialized) return;
  40. 40.this.handleValueChange(newVal);
  41. 41.},
  42. 42.},
  43. 43.},
  44. 44.mounted() {
  45. 45.this._isInitialized = true;
  46. 46.// Now the watcher will fire for subsequent changes only
  47. 47.},
  48. 48.};
  49. 49.`
  50. 50.In Vue 3 Composition API, use watchEffect or separate watchers:
  51. 51.```javascript
  52. 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. 1.Cancel in-flight requests when the watcher fires again:
  2. 2.```javascript
  3. 3.watch: {
  4. 4.userId: {
  5. 5.async handler(newId) {
  6. 6.if (this._abortController) {
  7. 7.this._abortController.abort();
  8. 8.}
  9. 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 } OR onMounted, 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 watchEffect for 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