intermediate Step 12 of 15

Lifecycle Hooks

Vue.js Development

Lifecycle Hooks

Every Vue component goes through a series of initialization steps when it is created — setting up reactive data, compiling the template, mounting the instance to the DOM, and updating the DOM when data changes. Vue exposes lifecycle hooks that let you run code at specific stages of this process. Common use cases include fetching data when a component mounts, setting up event listeners or timers, cleaning up resources when a component is destroyed, and reacting to DOM updates. Understanding the lifecycle helps you write efficient components that correctly manage side effects.

Lifecycle Diagram

The main lifecycle stages are: creation (setup/beforeCreate/created), mounting (beforeMount/mounted), updating (beforeUpdate/updated), and unmounting (beforeUnmount/unmounted). In the Composition API, these map to onMounted, onUpdated, onUnmounted, and their "before" counterparts.

Options API Hooks

<script>
export default {
  data() {
    return {
      users: [],
      timer: null,
    };
  },

  // Called after instance is created, before mounting
  created() {
    console.log("Component created — data is reactive");
    // Good for: initial API calls, setting up non-DOM logic
    this.fetchUsers();
  },

  // Called after component is mounted to the DOM
  mounted() {
    console.log("Component mounted — DOM is available");
    // Good for: DOM manipulation, third-party library init, timers
    this.timer = setInterval(() => {
      this.fetchUsers();
    }, 30000);

    // Access DOM elements via $refs
    this.$refs.searchInput?.focus();
  },

  // Called after reactive data change causes DOM update
  updated() {
    console.log("DOM updated after data change");
    // Good for: post-update DOM operations
    // Caution: avoid changing state here (infinite loop risk)
  },

  // Called before component is unmounted
  beforeUnmount() {
    console.log("About to unmount — clean up!");
    // Good for: clearing timers, removing event listeners
    clearInterval(this.timer);
  },

  methods: {
    async fetchUsers() {
      const response = await fetch("/api/users");
      this.users = await response.json();
    },
  },
};
</script>

Composition API Hooks

<script setup>
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from "vue";

const users = ref([]);
const searchInput = ref(null); // template ref
let timer = null;

async function fetchUsers() {
  const response = await fetch("/api/users");
  users.value = await response.json();
}

onBeforeMount(() => {
  console.log("Before mount — DOM not yet available");
});

onMounted(() => {
  console.log("Mounted — DOM is ready");
  fetchUsers();

  // Start polling
  timer = setInterval(fetchUsers, 30000);

  // Focus the search input
  searchInput.value?.focus();
});

onBeforeUpdate(() => {
  console.log("Before DOM update");
});

onUpdated(() => {
  console.log("DOM updated");
});

onBeforeUnmount(() => {
  console.log("Before unmount — last chance to clean up");
});

onUnmounted(() => {
  console.log("Unmounted — component destroyed");
  clearInterval(timer);
});
</script>

<template>
  <div>
    <input ref="searchInput" placeholder="Search users..." />
    <ul>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

Practical Lifecycle Patterns

<script setup>
import { ref, onMounted, onUnmounted } from "vue";

// Pattern 1: Event listener cleanup
const windowWidth = ref(window.innerWidth);
function handleResize() {
  windowWidth.value = window.innerWidth;
}

onMounted(() => window.addEventListener("resize", handleResize));
onUnmounted(() => window.removeEventListener("resize", handleResize));

// Pattern 2: Third-party library integration
let chartInstance = null;
const chartContainer = ref(null);

onMounted(() => {
  // Initialize chart library after DOM is ready
  // chartInstance = new Chart(chartContainer.value, { ... });
});

onUnmounted(() => {
  // Destroy chart to prevent memory leaks
  chartInstance?.destroy();
});

// Pattern 3: AbortController for fetch cleanup
const data = ref(null);
let abortController = null;

onMounted(async () => {
  abortController = new AbortController();
  try {
    const response = await fetch("/api/data", {
      signal: abortController.signal,
    });
    data.value = await response.json();
  } catch (e) {
    if (e.name !== "AbortError") console.error(e);
  }
});

onUnmounted(() => {
  abortController?.abort(); // Cancel in-flight request
});
</script>
Tip: Always clean up side effects in onUnmounted (or beforeUnmount). This includes clearing intervals and timeouts, removing event listeners, aborting fetch requests, and destroying third-party library instances. Failing to clean up causes memory leaks.

Key Takeaways

  • onMounted runs after the component's DOM is ready — use it for DOM access and API calls.
  • onUnmounted runs when the component is destroyed — use it to clean up timers, listeners, and subscriptions.
  • onUpdated runs after DOM updates — avoid changing state inside to prevent infinite loops.
  • In the Composition API, lifecycle hooks are imported functions called inside setup or <script setup>.
  • Always pair setup with cleanup: addEventListener with removeEventListener, setInterval with clearInterval.