# Props and Events

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Components
Source: https://www.devtutorials.ai/learn/vue/components/props-and-events

> defineProps() with runtime and type-based declarations, prop types, required props, defaults and validators, reactive props destructure, one-way data flow, deriving local values, $emit and defineEmits(), typed events, component v-model with defineModel(), many models and modifiers, and how components talk.

## Talking Between Components

- A parent often needs to give data to a child.
- A child often needs to tell the parent something happened.
- **Props** = data passed **down** from parent to child.
- **Events** = messages sent **up** from child to parent.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-props" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-props">Animation: the parent App sends props name and price down to DishCard. DishCard sends an add event back up to App</title>
  <rect x="220" y="15" width="200" height="50" rx="12" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
  <text x="320" y="46" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 15, fontWeight: 700 }}>App (parent)</text>
  <rect x="220" y="135" width="200" height="50" rx="12" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
  <text x="320" y="166" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 15, fontWeight: 700 }}>DishCard (child)</text>
  <path d="M280 65 V133" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2.5 }} />
  <path d="M360 133 V67" style={{ stroke: "var(--ngc-orange)", strokeWidth: 2.5 }} />
  <text x="270" y="104" textAnchor="end" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>props ↓ name, price</text>
  <text x="370" y="104" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>events ↑ add</text>
  <circle r="7" cx="280" style={{ fill: "var(--ngc-purple)" }}><animate attributeName="cy" values="65;133;133" keyTimes="0;0.4;1" dur="5s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.4;0.41;1" dur="5s" repeatCount="indefinite" /></circle>
  <circle r="7" cx="360" opacity="0" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cy" values="133;133;67;67" keyTimes="0;0.5;0.9;1" dur="5s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5;0.51;0.9;0.91" dur="5s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>Props go down. Events go up.</figcaption>
</figure>

**Simple lesson:** props down, events up.

### A Real-World Example

- Meera's menu uses one `DishCard` for every dish.
- The parent passes each card a name and a price.
- When someone taps **Add**, the card tells the parent, and the parent updates the cart.

const cart = ref<string[]>([]);
</script>

<template>
    <p>Cart: {{ cart.join(", ") || "empty" }}</p>
</template>
`,
    "DishCard.vue": `<script setup lang="ts">
const props = defineProps<{ name: string; price: number }>();
const emit = defineEmits<{ add: [name: string] }>();
</script>

<template>
    <div class="card">
      <b>{{ name }}</b> ₹{{ price }}
      <button @click="emit('add', props.name)">Add</button>
    </div>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "DishCard.vue": `<script setup>
const props = defineProps({
    name: { type: String, required: true },
    price: { type: Number, required: true },
});
const emit = defineEmits(["add"]);
</script>

<template>
    <div class="card">
      <b>{{ name }}</b> ₹{{ price }}
      <button @click="emit('add', props.name)">Add</button>
    </div>
</template>
`,
  }}
/>

- **What happened:** each card got its own `name` and `price`.
- **Add** sent an `add` event with the dish name. The parent pushed it into the cart.
- Switch the top bar to JavaScript to see the same component without types.

### What You Will Learn

1. How to declare props with `defineProps()`.
2. Two ways to declare them: runtime options and TypeScript types.
3. Required props, defaults and validators.
4. Why a child must not change its props.
5. How to send events with `$emit` and `defineEmits()`.
6. How to send data with an event.
7. How to make your own component work with `v-model`.
8. How to use many `v-model`s and custom modifiers.
9. Which way to pass data in each situation.

## defineProps()

- `defineProps()` lists the props a component accepts.
- It is a **compiler macro**: you do not import it.
- It returns an object with the prop values. In the template, use the names directly.
- Pass a fixed text: `name="Idli"`. Pass JavaScript (numbers, variables): `:price="50"`.
- Name props in camelCase in the script (`dishName`). In templates both `:dishName` and `:dish-name` work.
- A `Boolean` prop written with no value is `true`: `<DishCard featured />`. Left out, it is `false`.

### Runtime Declaration

- Describe each prop with an object: `type`, `required`, `default` and `validator`.
- This works in TypeScript and JavaScript, and Vue checks the values while the app runs.
- A `default` for an object or array must be a function that returns it.

</script>

<template>
</template>
`,
    "SpiceMeter.vue": `<script setup lang="ts">
const props = defineProps({
    dish: { type: String, required: true },
    level: {
      type: Number,
      default: 0,
      validator: (value: number) => value >= 0 && value <= 3,
    },
    tags: { type: Array, default: () => [] },
});
</script>

<template>
    <p>{{ dish }}: {{ "🌶".repeat(level) || "not spicy" }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

A failed check, such as a missing required prop or a validator that returns false, gives a Vue warning in the Console. It does not stop the app. In production builds, these checks are skipped.

### Type-Based Declaration (TypeScript)

- In TypeScript, give the props as a type: `defineProps<{ name: string; price?: number }>()`.
- `?` makes a prop optional.
- Since Vue 3.5, you can destructure props and give defaults in one line. They stay reactive.
- Older code uses `withDefaults(defineProps<...>(), { price: 0 })` for the same job.
- The JavaScript flavour shows the runtime version of the same component.

const plates = ref<number>(1);
</script>

<template>
    <button @click="plates++">More idli</button>
</template>
`,
    "OrderLine.vue": `<script setup lang="ts">
const { dish, plates = 1, price = 50 } = defineProps<{
    dish: string;
    plates?: number;
    price?: number;
}>();
</script>

<template>
    <p>{{ plates }} × {{ dish }} = ₹{{ plates * price }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "OrderLine.vue": `<script setup>
const { dish, plates = 1, price = 50 } = defineProps({
    dish: { type: String, required: true },
    plates: Number,
    price: Number,
});
</script>

<template>
    <p>{{ plates }} × {{ dish }} = ₹{{ plates * price }}</p>
</template>
`,
  }}
/>

**Simple lesson:** list props with `defineProps()`. Use a type in TypeScript, or runtime options anywhere.

## One-Way Data Flow

- Props flow down only. When the parent's value changes, the child gets the new value.
- A child must **never** change a prop. Props are read-only.
- If a child changes a prop, Vue warns, and the parent may overwrite it later.
- Need to change it? Copy it into a local ref, or work out a new value with `computed`.
- A copy (`ref(props.price)`) takes the **starting** value only. If the parent changes the prop later, the copy does not follow. Use `computed` when it must follow.

</script>

<template>
</template>
`,
    "Counter.vue": `<script setup lang="ts">

const props = defineProps({ start: { type: Number, required: true } });

const count = ref<number>(props.start);
const doubled = computed<number>(() => props.start * 2);

function wrong(): void {
    (props as { start: number }).start++;
}
</script>

<template>
    <p>Local copy: {{ count }} <button @click="count++">+1 (local)</button></p>
    <p>Derived: {{ doubled }}</p>
    <button @click="wrong">Change the prop (wrong)</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** "+1 (local)" changed the child's own copy. That is fine.
- "Change the prop" was blocked, and Vue wrote a warning in the Console.

**Simple lesson:** never change a prop. Copy it, or derive from it.

A local copy made with `ref(props.start)` does not follow later changes from the parent. Use `computed` when the child must always match the prop.

## Events

- A child sends an event with `$emit('name', data)` in the template.
- In the script, declare events with `defineEmits()`. It returns an `emit` function.
- The parent listens with `@name="handler"`, like a normal DOM event.
- Extra arguments to `emit` reach the handler. In an inline handler they are `$event`.

const rating = ref<number>(0);
const log = ref<string>("");

function onRate(stars: number, dish: string): void {
    rating.value = stars;
    log.value = \`\${dish} got \${stars} stars\`;
}
</script>

<template>
    <p>Rating: {{ rating }}. {{ log }}</p>
</template>
`,
    "RatingStars.vue": `<script setup lang="ts">
const props = defineProps<{ dish: string }>();
const emit = defineEmits<{
    rate: [stars: number, dish: string];
    clear: [];
}>();
</script>

<template>
    <button v-for="n in 5" :key="n" @click="emit('rate', n, props.dish)">{{ n }}★</button>
    <button @click="$emit('clear')">Clear</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "RatingStars.vue": `<script setup>
const props = defineProps({ dish: { type: String, required: true } });
const emit = defineEmits(["rate", "clear"]);
</script>

<template>
    <button v-for="n in 5" :key="n" @click="emit('rate', n, props.dish)">{{ n }}★</button>
    <button @click="$emit('clear')">Clear</button>
</template>
`,
  }}
/>

- **What happened:** each star button emitted `rate` with two values.
- The parent's `onRate(stars, dish)` received both.
- **Clear** used `$emit` straight from the template.

In TypeScript, `defineEmits<{ rate: [stars: number] }>()` types each event's data. Calling `emit('rate', 'five')` is then marked as an error in your editor. Runtime validation is also possible with an object of functions.

**Simple lesson:** declare events with `defineEmits()`. Send them with `emit()`. Listen with `@`.

## v-model on Components

- `v-model` on a component = a prop down + an update event up.
- Since Vue 3.4, `defineModel()` creates both for you.
- It returns a ref. Change it in the child, and the parent's ref changes too.
- `v-model:name` gives a model a name, so one component can have many.

const plates = ref<number>(1);
const first = ref<string>("Meera");
const last = ref<string>("Nair");
</script>

<template>
    <p>Parent sees: {{ plates }} plates</p>
    <p>Booking for {{ first }} {{ last }}</p>
</template>
`,
    "QuantityPicker.vue": `<script setup lang="ts">
const count = defineModel<number>({ default: 1 });
</script>

<template>
    <button @click="count = Math.max(1, count - 1)">−</button>
    {{ count }}
    <button @click="count++">+</button>
</template>
`,
    "GuestName.vue": `<script setup lang="ts">
const first = defineModel<string>("first");
const last = defineModel<string>("last");
</script>

<template>
    <input v-model="first" placeholder="First name" />
    <input v-model="last" placeholder="Last name" />
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Custom Modifiers

- A parent can add its own modifier: `v-model.capitalize="dish"`.
- The child reads it from the second value of `defineModel()`.
- A `set` function changes the value before it reaches the parent.

const dish = ref<string>("");
</script>

<template>
    <p>Saved as: "{{ dish }}"</p>
</template>
`,
    "DishInput.vue": `<script setup lang="ts">
const [model, modifiers] = defineModel<string>({
    set(value: string) {
      if (modifiers.capitalize) return value.charAt(0).toUpperCase() + value.slice(1);
      return value;
    },
});
</script>

<template>
    <input v-model="model" placeholder="Dish name" />
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `defineModel()` gives a component `v-model` support in one line.

## Which Way to Pass Data?

<div className="tscroll">

| Need | Use | Lesson |
|---|---|---|
| Parent gives data to a child | Props | this lesson |
| Child tells the parent something happened | Events | this lesson |
| Two-way value between parent and child | `v-model` with `defineModel()` | this lesson |
| Many levels down, skipping the middle | `provide` / `inject` | **Slots, Attributes and Provide/Inject** |
| Data shared by far-apart components | A Pinia store | **State Management with Pinia** |

</div>

**Simple lesson:** props down, events up, `v-model` for both, and other tools for long distances.

## TL;DR

- **Props** = data passed down. Declared with `defineProps()`.
- **Runtime props** = `{ type, required, default, validator }`. Works in JS and TS.
- **Type-based props** = `defineProps<{ name: string }>()`. TypeScript only.
- **Props destructure** = `const { price = 0 } = defineProps(...)` stays reactive (Vue 3.5).
- **One-way data flow** = a child never changes its props.
- **Events** = `emit('name', data)` up. `@name` in the parent.
- **`defineEmits()`** = declares events. Types each event's data in TS.
- **`defineModel()`** = a prop + update event as one ref, for `v-model`.
- **`v-model:name`** = many models on one component.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Changing a prop in the child | Props are like local variables | Copy into a ref, or emit an event | A warning, and the parent overwrites it |
| `price="50"` for a number | Vue guesses the type | Write `:price="50"` | The child gets the text "50" |
| Object default without a function | `default: []` is fine | `default: () => []` | All instances share one array |
| Forgetting `defineEmits` | `emit` exists by itself | `const emit = defineEmits([...])` | `emit` is not defined |
| Listening for an event the child never sends | Any `@name` works | Match a name from `defineEmits` | The handler never runs |
| Type-based props in a `.js` file | Types work everywhere | Use runtime options in JavaScript | A syntax error in JavaScript |
| Local ref for `v-model` in the child | The parent sees the local ref | Use `defineModel()` | The parent's value never changes |

</div>

## Check Yourself

## Exercises

## What to Remember

- Props go down, events go up.
- Declare props with runtime options, or with a type in TypeScript.
- Never change a prop. Copy it, derive from it, or emit an event.
- `defineEmits()` declares events. `emit('name', data)` sends them.
- `defineModel()` makes a component work with `v-model`.

Next: **Slots, Attributes and Provide/Inject**
