Props
Vue props: defineProps, types, required vs optional, defaults, validation, and the one-way data flow rule.
Vue — props
EXAMPLE
<!-- ===== Basic props ===== -->
<!-- ChildComp.vue -->
<script setup>
const props = defineProps(['title', 'count']);
console.log(props.title);
</script>
<template>
<h2>{{ title }} ({{ count }})</h2>
</template>
<!-- Use: -->
<ChildComp title="Hello" :count="3" />
<!-- ===== Typed props (TS or object form) ===== -->
<script setup lang="ts">
defineProps<{
title: string;
count?: number;
user: { id: number; name: string };
}>();
</script>
<!-- ===== Object syntax with defaults + validation ===== -->
<script setup>
defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
user: { type: Object, required: true },
tags: { type: Array, default: () => [] }, // defaults for objects / arrays must be factories
status: {
type: String,
default: 'new',
validator: (v) => ['new', 'shipped', 'cancelled'].includes(v),
},
});
</script>
<!-- ===== With defaults helper (script setup) ===== -->
<script setup>
const props = withDefaults(defineProps<{
title: string;
count?: number;
active?: boolean;
}>(), {
count: 0,
active: false,
});
</script>
<!-- ===== Boolean attribute shorthand ===== -->
<MyChip active /> <!-- shorthand for :active="true" -->
<MyChip :active="false" />
<!-- ===== One-way data flow rule ===== -->
<!-- Parent owns the value; child MUST NOT mutate the prop directly. -->
<script setup>
const props = defineProps(['count']);
// props.count = 99; // dev warning + bug
// Convert to local if you need a writable copy:
import { ref, watch } from 'vue';
const local = ref(props.count);
watch(() => props.count, (v) => local.value = v);
</script>
<!-- For two-way communication, prefer events: -->
<button @click="$emit('update:count', local + 1)">+</button>
<!-- Parent: <Child v-model:count="value" /> -->
<!-- ===== Passing as v-model ===== -->
<!-- Child.vue -->
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<!-- Parent uses v-model: -->
<Child v-model="name" />
<!-- ===== Static vs dynamic ===== -->
<MyChip color="blue" /> <!-- static, always 'blue' -->
<MyChip :color="theme.color" /> <!-- dynamic, reactive -->
<!-- ===== Destructuring (be careful) ===== -->
<script setup>
const props = defineProps(['title']);
const { title } = props; // breaks reactivity if title changes; rarely what you want
// Use computed for reactive transforms:
import { computed } from 'vue';
const upper = computed(() => props.title.toUpperCase());
</script>
<!-- ===== Patterns to internalise =====
- Type props (TS) or object form with required / default / validator
- Defaults for arrays / objects as factory functions
- One-way down; emit events for change
- v-model:fieldName for two-way binding sugar
-->
<!-- ===== Pitfalls =====
- Mutating props directly (dev warning + bug)
- Destructuring props in setup -> loses reactivity
- Forgetting factory defaults for objects/arrays
- Boolean props passed as string 'false' (which is truthy)
-->
Why it matters
Props are the contract between parent and child: types, required, defaults, validator. One-way down; use events or v-model:field for change. Type with TS or object form, factory defaults for objects/arrays, computed for derived values, and props.x.subfield modifications happen at the parent.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<!-- Child.vue -->
<script setup>
defineProps({ name: String, age: { type: Number, default: 0 } });
</script>
<template>
<h2>{{ name }} ({{ age }})</h2>
</template>
Try it Yourself »
Discussion
Loading…