Vue.js Custom Events

Props carry data from parent to child. Custom events carry signals from child back to parent. Together, they create the complete communication system between components.

Why Child Components Need to Signal the Parent

A child component cannot modify props directly — props are read-only. But the child often needs to tell the parent that something happened: a button was clicked, a form was submitted, an item was deleted. Custom events are the correct way to send these signals upward.

Diagram: Communication Direction

┌──────────────────────────────────────────────┐
│              Parent Component                │
│                                              │
│  data: { message: "Hello" }                  │
│                                              │
│    Props (data DOWN) ──────────────────────▶ │
│    Custom Events (signals UP) ◀────────────  │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │          Child Component               │  │
│  │  receives: message prop                │  │
│  │  emits: "update-message" event         │  │
│  └────────────────────────────────────────┘  │
└──────────────────────────────────────────────┘

Emitting a Custom Event

A child component emits an event using this.$emit(). The first argument is the event name. Additional arguments are data to send along with the event.

// Child component
app.component("like-button", {
  data() {
    return { liked: false };
  },
  methods: {
    toggleLike() {
      this.liked = !this.liked;
      this.$emit("like-changed", this.liked);
    }
  },
  template: `
    <button @click="toggleLike">
      {{ liked ? "❤ Liked" : "♡ Like" }}
    </button>
  `
});
// Parent template listens for the event
<div id="app">
  <like-button @like-changed="handleLike"></like-button>
  <p>Status: {{ likeStatus }}</p>
</div>

<script>
  const app = Vue.createApp({
    data() {
      return { likeStatus: "Not liked" };
    },
    methods: {
      handleLike(isLiked) {
        this.likeStatus = isLiked ? "You liked this!" : "Like removed.";
      }
    }
  });
  // ... register like-button component, then:
  app.mount("#app");
</script>

Diagram: Event Emission Flow

User clicks the "Like" button
        │
        ▼
Child toggleLike() runs
  this.liked = true
  this.$emit("like-changed", true)
        │
        │  event travels UP
        ▼
Parent @like-changed fires → calls handleLike(true)
  this.likeStatus = "You liked this!"
        │
        ▼
Parent template updates:
  Status: You liked this!

Declaring Emits with the emits Option

Vue 3 introduced the emits option to explicitly list all events a component can emit. This documents the component's interface and prevents warnings about unrecognized native events.

app.component("search-bar", {
  emits: ["search-submitted", "search-cleared"],
  data() {
    return { query: "" };
  },
  methods: {
    submitSearch() {
      this.$emit("search-submitted", this.query);
    },
    clearSearch() {
      this.query = "";
      this.$emit("search-cleared");
    }
  },
  template: `
    <div>
      <input v-model="query" placeholder="Search...">
      <button @click="submitSearch">Search</button>
      <button @click="clearSearch">Clear</button>
    </div>
  `
});

Emitting Events with Payload Data

You can send any data along with an emitted event — strings, numbers, objects, or arrays. The parent receives the data as arguments in its handler method.

// Child emits an object as the payload
this.$emit("item-added", {
  id: Date.now(),
  name: this.newItemName,
  quantity: this.quantity
});

// Parent receives the object
methods: {
  handleItemAdded(item) {
    this.cartItems.push(item);
    console.log("Added:", item.name, "x", item.quantity);
  }
}

Diagram: Payload Travel

Child emits:
  this.$emit("item-added", { id: 101, name: "Notebook", quantity: 2 })
                                         │
                                         │ payload travels with event
                                         ▼
Parent handler receives:
  handleItemAdded({ id: 101, name: "Notebook", quantity: 2 })
                                         │
                                         ▼
Parent pushes item to cartItems array
Vue updates the cart display

v-model on Custom Components

You can use v-model on a custom component to create two-way binding between the parent and child. Under the hood, Vue passes a modelValue prop and listens for an update:modelValue event.

// Child component — a custom input
app.component("custom-input", {
  props: ["modelValue"],
  emits: ["update:modelValue"],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      placeholder="Type here..."
    >
  `
});
// Parent uses v-model directly on the component
<custom-input v-model="searchText"></custom-input>
<p>Searching for: {{ searchText }}</p>

Diagram: v-model on a Custom Component

v-model="searchText" expands to:
  :modelValue="searchText"          (prop going into child)
  @update:modelValue="searchText = $event"  (event from child)

User types "Vue":
  Child emits update:modelValue with "Vue"
  Parent sets searchText = "Vue"
  Parent passes searchText="Vue" back as modelValue
  Input shows "Vue"

Everything stays in sync automatically.

Event Validation

Like prop validation, you can validate emitted events by providing an object instead of an array in emits.

emits: {
  // No validation
  "search-cleared": null,

  // With validation function
  "search-submitted": (query) => {
    if (typeof query !== "string") {
      console.warn("search-submitted requires a string payload");
      return false;
    }
    return true;
  }
}

Full Example: Todo Item with Delete

A common pattern is a list item that signals the parent to remove itself.

// Child: todo-item
app.component("todo-item", {
  props: {
    item: { type: Object, required: true }
  },
  emits: ["delete-item"],
  template: `
    <li>
      {{ item.text }}
      <button @click="$emit('delete-item', item.id)">Delete</button>
    </li>
  `
});
// Parent
<div id="app">
  <ul>
    <todo-item
      v-for="todo in todos"
      :key="todo.id"
      :item="todo"
      @delete-item="removeItem"
    ></todo-item>
  </ul>
</div>

<script>
  const app = Vue.createApp({
    data() {
      return {
        todos: [
          { id: 1, text: "Buy groceries" },
          { id: 2, text: "Walk the dog" },
          { id: 3, text: "Read a book" }
        ]
      };
    },
    methods: {
      removeItem(id) {
        this.todos = this.todos.filter(todo => todo.id !== id);
      }
    }
  });
  // Register todo-item component...
  app.mount("#app");
</script>

Diagram: Delete Flow

List:
  • Buy groceries  [Delete]
  • Walk the dog   [Delete]   ← user clicks this
  • Read a book    [Delete]

Child emits: $emit("delete-item", 2)
        │
        ▼
Parent: removeItem(2)
  todos = todos.filter(t => t.id !== 2)
  todos = [{ id:1 ... }, { id:3 ... }]
        │
        ▼
Vue re-renders the list:
  • Buy groceries  [Delete]
  • Read a book    [Delete]

Summary

Custom events complete the parent-child communication loop. A child emits events using this.$emit("event-name", payload). The parent listens with @event-name="handler". Declare emitted events explicitly using the emits option for cleaner, self-documenting components. Send any data as the payload — strings, numbers, or objects. Use v-model on a custom component to wire up two-way binding through modelValue and update:modelValue automatically.

Leave a Comment

Your email address will not be published. Required fields are marked *