Cách sử dụng Props trong Vue 3
Khi xây dựng ứng dụng Vue, bạn sẽ rất nhanh gặp khái niệm props.
Props là cơ chế chính để component cha truyền dữ liệu xuống component con.
Ví dụ:
<UserCard
name="Alice"
age="28"
/>Component UserCard có thể nhận name và age làm props rồi sử dụng chúng để render giao diện.
Nếu bạn đã từng truyền dữ liệu giữa các component nhưng chưa thực sự hiểu:
- Props hoạt động như thế nào?
- Khi nào nên dùng
:trước prop? - Làm thế nào để khai báo props trong Vue 3?
- Làm sao để sử dụng TypeScript với props?
- Props có thể có giá trị mặc định không?
- Làm thế nào để bắt buộc một prop?
- Có thể thay đổi props trong component con không?
- Object và Array props có gì đặc biệt?
- Props có còn reactive khi destructure không?
- Làm thế nào để sử dụng props với
computed()vàwatch()?
Thì đây là những phần quan trọng cần nắm vững.
Props là gì?
Props là dữ liệu được component cha truyền xuống component con.
Bạn có thể hình dung nó tương tự như việc truyền argument vào một function.
Ví dụ JavaScript:
function greet(name) {
return `Hello, ${name}`
}
greet('Alice')Ở đây:
'Alice'
↓
name
↓
greet()Props hoạt động theo ý tưởng tương tự:
Parent component
↓
props
↓
Child componentVí dụ:
<UserCard name="Alice" />Component cha truyền giá trị:
name = "Alice"Xuống component UserCard.
Component con khai báo rằng nó nhận prop name:
<script setup lang="ts">
const props = defineProps<{
name: string
}>()
</script>
<template>
<h2>{{ props.name }}</h2>
</template>Kết quả:
AliceProps giúp component trở nên có thể tái sử dụng.
Thay vì tạo một component chỉ dành cho một người dùng cụ thể, bạn có thể tạo:
<UserCard name="Alice" />
<UserCard name="Bob" />
<UserCard name="Carol" />Và cùng sử dụng một component.
One-way data flow
Props trong Vue tuân theo nguyên tắc one-way-down data flow.
Dữ liệu đi từ:
Parent
↓
Child
↓
GrandchildMột component con không nên thay đổi trực tiếp prop mà component cha đã truyền xuống.
Ví dụ:
<script setup lang="ts">
const props = defineProps<{
title: string
}>()
props.title = 'New title'
</script>Đây là cách sử dụng không đúng.
Props là readonly trong component con.
Nếu component cha thay đổi giá trị:
Parent state
↓
prop
↓
ChildGiá trị mới sẽ được truyền xuống component con.
Nhưng component con không nên trực tiếp ghi ngược vào prop:
Child
✕
↑
ParentĐiều này giúp luồng dữ liệu dễ theo dõi hơn và giảm tình trạng state bị thay đổi từ nhiều nơi.
Truyền một prop vào component con
Giả sử chúng ta có component:
<script setup lang="ts">
const props = defineProps<{
title: string
}>()
</script>
<template>
<h2>{{ props.title }}</h2>
</template>Component cha có thể truyền prop:
<UserCard title="Vue 3 Guide" />Trong trường hợp này:
title
↓
"Vue 3 Guide"Được truyền từ parent vào child.
Static props và dynamic props
Có một điểm rất quan trọng khi truyền props.
Nếu bạn viết:
<UserCard name="Alice" />Thì "Alice" là một chuỗi literal.
Nếu bạn muốn truyền giá trị từ JavaScript, hãy sử dụng v-bind, thường viết dưới dạng ::
<script setup lang="ts">
const userName = 'Alice'
</script>
<template>
<UserCard :name="userName" />
</template>Hai cách này có ý nghĩa khác nhau:
<UserCard name="Alice" />Truyền một String literal.
Trong khi:
<UserCard :name="userName" />Đánh giá expression userName và truyền kết quả của expression đó.
Truyền Number
Đây là một lỗi khá phổ biến.
Nếu bạn viết:
<ProductCard rating="5" />Thì giá trị được truyền là String:
'5'Không phải Number:
5Nếu component cần Number, hãy sử dụng v-bind:
<ProductCard :rating="5" />Hoặc:
<script setup lang="ts">
const rating = 5
</script>
<template>
<ProductCard :rating="rating" />
</template>Khi đó prop nhận được Number:
5Quy tắc đơn giản:
prop="value"Thường truyền giá trị dạng attribute/String, còn:
:prop="value"Sẽ đánh giá value như một JavaScript expression.
Truyền Boolean
Boolean props có behavior đặc biệt trong Vue.
Ví dụ component khai báo:
<script setup lang="ts">
const props = defineProps<{
disabled: boolean
}>()
</script>Bạn có thể sử dụng:
<Button disabled />Đây tương đương với:
<Button :disabled="true" />Nếu không truyền prop:
<Button />Với runtime Boolean prop thông thường, Vue sẽ xử lý giá trị vắng mặt theo Boolean casting rules.
Boolean props đặc biệt hữu ích cho các API component như:
<Modal :open="isOpen" />
<Button disabled />
<Dialog fullscreen />Khai báo props với defineProps()
Trong Vue 3 hiện đại, đặc biệt khi sử dụng <script setup>, defineProps() là cách phổ biến để khai báo props.
Ví dụ:
<script setup lang="ts">
const props = defineProps<{
title: string
count: number
}>()
</script>
<template>
<h2>{{ props.title }}</h2>
<p>{{ props.count }}</p>
</template>Ở đây component nhận hai props:
title → string
count → numberTypeScript cũng có thể kiểm tra việc sử dụng các props này.
Props bắt buộc và tùy chọn
Trong type-based declaration:
defineProps<{
title: string
description?: string
}>()title là required:
title: stringTrong khi description là optional:
description?: stringDo đó:
<MyComponent title="Hello" />Là hợp lệ, nhưng:
<MyComponent />Sẽ thiếu prop bắt buộc title.
Có thể hiểu:
title: string
→ required
description?: string
→ optionalĐây là một trong những lợi ích lớn khi sử dụng TypeScript với props.
Runtime declaration và type-based declaration
Vue hỗ trợ hai cách chính để khai báo props trong <script setup>.
Runtime declaration
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
</script>Ở đây Vue sử dụng object declaration để tạo runtime prop options.
Type-based declaration
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
</script>Cách thứ hai thường rất thuận tiện khi sử dụng TypeScript.
Vue có thể suy ra runtime prop information từ type declaration trong những trường hợp mà compiler có thể phân tích được.
Bạn nên chọn một trong hai cách cho một lần khai báo defineProps():
defineProps({
// runtime declaration
})Hoặc:
defineProps<Props>()Không sử dụng cả hai cùng lúc.
Khi nào nên dùng runtime declaration?
Runtime declaration hữu ích khi bạn cần những tính năng như:
- Runtime type checking.
required.default.validator.- Các runtime prop options khác.
Ví dụ:
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
required: true
},
rating: {
type: Number,
default: 0
}
})
</script>Khi nào nên dùng type-based declaration?
Nếu dự án sử dụng TypeScript và props chủ yếu được mô tả bằng type/interface, type-based declaration thường rõ ràng hơn:
<script setup lang="ts">
interface Props {
title: string
rating?: number
description?: string
}
const props = defineProps<Props>()
</script>Cách này đặc biệt phù hợp với các props có cấu trúc dữ liệu phức tạp.
Ví dụ:
interface Product {
id: number
name: string
price: number
}
interface Props {
product: Product
}Sau đó:
<script setup lang="ts">
interface Product {
id: number
name: string
price: number
}
interface Props {
product: Product
}
const props = defineProps<Props>()
</script>
<template>
<article>
<h2>{{ props.product.name }}</h2>
<p>{{ props.product.price }}</p>
</article>
</template>TypeScript sẽ hiểu cấu trúc của product.
Giá trị mặc định cho props
Props optional có thể có giá trị mặc định.
Với Vue 3.5+, khi sử dụng type-based declaration, bạn có thể dùng Reactive Props Destructure:
<script setup lang="ts">
interface Props {
title?: string
count?: number
}
const {
title = 'Untitled',
count = 0
} = defineProps<Props>()
</script>
<template>
<h2>{{ title }}</h2>
<p>{{ count }}</p>
</template>Nếu parent không truyền:
<MyComponent />Thì component sẽ sử dụng:
title = "Untitled"
count = 0Cách này đặc biệt hữu ích khi sử dụng TypeScript.
withDefaults() trong các trường hợp cần thiết
Một cách khác để cung cấp default values cho type-based props là withDefaults():
<script setup lang="ts">
interface Props {
title?: string
labels?: string[]
}
const props = withDefaults(
defineProps<Props>(),
{
title: 'Untitled',
labels: () => ['One', 'Two']
}
)
</script>Với Array hoặc Object mutable, withDefaults() nên sử dụng factory function để mỗi component instance có một giá trị riêng.
Ví dụ:
labels: () => ['One', 'Two']Thay vì chia sẻ cùng một Array.
Default value với runtime declaration
Nếu sử dụng runtime declaration, có thể khai báo:
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
default: 'Untitled'
},
count: {
type: Number,
default: 0
}
})
</script>Nếu parent không truyền title hoặc count, Vue sẽ sử dụng các giá trị mặc định này.
Default value cho Object và Array
Với runtime declaration, Object và Array nên trả về một giá trị mới từ factory function.
Ví dụ:
<script setup lang="ts">
const props = defineProps({
options: {
type: Object,
default() {
return {
size: 'medium'
}
}
},
tags: {
type: Array,
default() {
return []
}
}
})
</script>Điều này đảm bảo mỗi component instance nhận được một Object hoặc Array riêng.
Prop validation
Props không chỉ dùng để truyền dữ liệu.
Bạn cũng có thể định nghĩa yêu cầu về dữ liệu.
Ví dụ:
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
required: true
},
rating: {
type: Number,
default: 0
}
})
</script>Vue có thể kiểm tra runtime type trong development build.
Nếu component yêu cầu:
rating: NumberNhưng nhận một giá trị không phù hợp, Vue có thể phát cảnh báo trong console.
Prop validation giúp phát hiện lỗi sớm khi component được sử dụng sai.
Một prop có thể chấp nhận nhiều kiểu
Runtime declaration cho phép khai báo nhiều kiểu:
<script setup lang="ts">
const props = defineProps({
id: [String, Number]
})
</script>Prop id có thể nhận:
<MyComponent id="user-1" />Hoặc:
<MyComponent :id="123" />Tuy nhiên, nếu sử dụng TypeScript, hãy cân nhắc liệu union type có thực sự cần thiết hay không.
Một API component rõ ràng thường dễ sử dụng và bảo trì hơn một prop có quá nhiều kiểu dữ liệu.
Custom validator
Runtime declaration cũng hỗ trợ custom validator.
Ví dụ một prop chỉ chấp nhận ba trạng thái:
<script setup lang="ts">
const props = defineProps({
status: {
type: String,
validator(value) {
return ['success', 'warning', 'error'].includes(value)
}
}
})
</script>Sau đó:
<Alert status="success" />Là hợp lệ, trong khi:
<Alert status="unknown" />Sẽ không đáp ứng validation.
Validator phù hợp khi một prop có các giá trị được giới hạn rõ ràng.
Tuy nhiên, nếu đang sử dụng TypeScript, hãy cân nhắc biểu diễn constraint ngay ở type level khi có thể:
type Status = 'success' | 'warning' | 'error'
interface Props {
status: Status
}Điều này giúp IDE và TypeScript phát hiện lỗi ngay trong quá trình phát triển.
Props là readonly
Một component con không nên thay đổi trực tiếp prop:
<script setup lang="ts">
const props = defineProps<{
count: number
}>()
props.count = 10
</script>Đây là cách sử dụng sai.
Thay vào đó, nếu component con cần yêu cầu parent thay đổi state, hãy sử dụng event hoặc một API component phù hợp.
Ví dụ:
<script setup lang="ts">
const props = defineProps<{
count: number
}>()
const emit = defineEmits<{
increment: []
}>()
</script>
<template>
<button
type="button"
@click="emit('increment')"
>
{{ props.count }}
</button>
</template>Parent có thể xử lý event:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<Counter
:count="count"
@increment="count++"
/>
</template>Luồng dữ liệu trở nên rõ ràng:
Parent state
↓
prop
↓
Child
↓
event
↓
ParentĐây là pattern quan trọng trong thiết kế component Vue.
Không nên mutate Object hoặc Array prop
Có một nuance quan trọng.
Nếu prop là Object hoặc Array:
<script setup lang="ts">
interface User {
name: string
}
const props = defineProps<{
user: User
}>()
</script>Component con không thể thay đổi binding:
props.user = {
name: 'Bob'
}Nhưng JavaScript cho phép thay đổi nested property:
props.user.name = 'Bob'Điều này có thể ảnh hưởng trực tiếp đến object mà parent đang sở hữu.
Vì vậy, dù kỹ thuật này có thể xảy ra, không nên coi nó là cách mặc định để giao tiếp giữa parent và child.
Nếu child cần yêu cầu thay đổi dữ liệu, tốt hơn nên để parent thực hiện mutation thông qua event hoặc một API state rõ ràng.
Khi prop chỉ là giá trị khởi tạo
Một trường hợp phổ biến là parent truyền một giá trị ban đầu, sau đó child muốn có state riêng.
Ví dụ:
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
initialCount: number
}>()
const count = ref(props.initialCount)
</script>Ở đây:
initialCount
↓
initial value
↓
local countSau khi count được khởi tạo, nó trở thành state riêng của component.
Điều này khác với việc tiếp tục đọc trực tiếp:
props.initialCountNếu mục tiêu là giữ giá trị luôn đồng bộ với parent, không nên copy nó thành local state theo cách này.
Khi prop cần được biến đổi
Một trường hợp khác là prop được truyền vào dưới dạng raw value nhưng component cần một phiên bản đã được chuẩn hóa.
Ví dụ:
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
size: string
}>()
const normalizedSize = computed(() => {
return props.size.trim().toLowerCase()
})
</script>
<template>
<div :data-size="normalizedSize">
Content
</div>
</template>Ở đây không mutate:
props.sizeMà tạo một giá trị dẫn xuất:
props.size
↓
computed()
↓
normalizedSizeĐây thường là cách phù hợp khi prop cần được transform.
Sử dụng props trong computed()
Props có thể được sử dụng làm dependency của computed().
Ví dụ:
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
firstName: string
lastName: string
}>()
const fullName = computed(() => {
return `${props.firstName} ${props.lastName}`
})
</script>
<template>
<h2>{{ fullName }}</h2>
</template>Nếu parent thay đổi:
firstNameHoặc:
lastNameThì fullName sẽ được cập nhật.
Đây là một use case tự nhiên của computed:
Prop là input, computed là derived state.
Sử dụng props trong watch()
Props cũng có thể được theo dõi bằng watch().
Ví dụ:
<script setup lang="ts">
import { watch } from 'vue'
const props = defineProps<{
userId: number
}>()
watch(
() => props.userId,
(newUserId, oldUserId) => {
console.log('Changed:', oldUserId, '→', newUserId)
}
)
</script>Cần chú ý rằng khi theo dõi một property của props, nên truyền getter:
watch(
() => props.userId,
callback
)Thay vì lấy giá trị hiện tại rồi truyền vào watch().
Reactive props destructure trong Vue 3.5+
Một thay đổi quan trọng trong Vue hiện đại là việc destructure props.
Bạn có thể viết:
<script setup lang="ts">
const { title } = defineProps<{
title: string
}>()
</script>
<template>
<h2>{{ title }}</h2>
</template>Trong Vue 3.5+, compiler hỗ trợ Reactive Props Destructure.
Điều này có nghĩa title vẫn được theo dõi khi prop thay đổi trong các trường hợp sử dụng phù hợp trong cùng <script setup>.
Ví dụ:
<script setup lang="ts">
import { watchEffect } from 'vue'
const { title } = defineProps<{
title: string
}>()
watchEffect(() => {
console.log(title)
})
</script>Khi title từ parent thay đổi, effect có thể phản ứng theo thay đổi đó.
Đây là khác biệt quan trọng so với Vue 3.4 trở về trước.
Destructure props không có nghĩa là mọi trường hợp đều giống nhau
Khi truyền một destructured prop vào API cần một reactive source, cần cẩn thận.
Ví dụ:
<script setup lang="ts">
import { watch } from 'vue'
const { userId } = defineProps<{
userId: number
}>()
watch(userId, () => {
// Không phải cách phù hợp để giữ reactive dependency
})
</script>Nếu cần theo dõi destructured prop, hãy dùng getter:
watch(
() => userId,
() => {
// ...
}
)Tương tự, khi truyền prop vào một composable và muốn giữ tính reactive, có thể truyền getter:
useSomething(() => userId)Thay vì truyền giá trị hiện tại.
Props và composables
Props thường được đưa vào composable để tái sử dụng logic.
Ví dụ:
function useGreeting(name: () => string) {
return computed(() => {
return `Hello, ${name()}!`
})
}Component:
<script setup lang="ts">
const { name } = defineProps<{
name: string
}>()
const greeting = useGreeting(() => name)
</script>
<template>
<p>{{ greeting }}</p>
</template>Điểm quan trọng là composable có thể nhận một getter để giữ mối liên hệ reactive với prop.
Điều này đặc biệt hữu ích khi xây dựng composable dùng lại ở nhiều component.
Props không phải là cách duy nhất để giao tiếp giữa components
Props phù hợp cho:
Parent → ChildNhưng khi cần giao tiếp theo chiều ngược lại:
Child → Parentthông thường bạn sẽ sử dụng:
emitVí dụ:
<script setup lang="ts">
const emit = defineEmits<{
save: [value: string]
}>()
function save() {
emit('save', 'Hello')
}
</script>
<template>
<button
type="button"
@click="save"
>
Save
</button>
</template>Parent:
<Editor @save="handleSave" />Props và emits thường được sử dụng cùng nhau để tạo ra một component API rõ ràng:
Props
Parent → Child
Emits
Child → ParentProps và v-model
Khi xây dựng component có hai chiều tương tác, v-model là một abstraction quan trọng.
Ví dụ một component có:
<CustomInput v-model="name" />Trong Vue 3, component có thể sử dụng defineModel() để làm việc với model value:
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input v-model="model" />
</template>Trong những trường hợp đơn giản, bạn có thể xem mối quan hệ này như:
Parent value
↓
model prop
↓
Child
↓
update event
↓
ParentVì vậy, khi thiết kế component, không nên cố sử dụng một prop readonly như thể nó là local mutable state.
Hãy chọn abstraction phù hợp:
Input data
→ props
Request parent change
→ emits
Two-way component value
→ v-model / defineModel()Đặt tên props
Trong <script setup> và JavaScript, convention phổ biến là camelCase:
defineProps<{
userName: string
}>()Khi truyền prop trong template, convention là kebab-case:
<UserCard user-name="Alice" />Ví dụ:
<script setup lang="ts">
const props = defineProps<{
userName: string
}>()
</script>Parent:
<UserCard user-name="Alice" />Cách này phù hợp với convention của HTML attributes và giúp template dễ đọc.
Không cần dùng this trong <script setup>
Code Vue 2 cũ thường truy cập props trong Options API thông qua:
this.nameTrong <script setup> hiện đại, bạn không làm như vậy.
Thay vào đó:
<script setup lang="ts">
const props = defineProps<{
name: string
}>()
console.log(props.name)
</script>Hoặc với reactive props destructure:
<script setup lang="ts">
const { name } = defineProps<{
name: string
}>()
console.log(name)
</script>Trong template:
<template>
<h2>{{ name }}</h2>
</template>Hoặc:
<template>
<h2>{{ props.name }}</h2>
</template>Đều có thể sử dụng tùy cách bạn khai báo props.
Một component hoàn chỉnh với props
Ví dụ dưới đây kết hợp:
- TypeScript.
- required prop.
- optional prop.
- default value.
- computed.
- template rendering.
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
title: string
description?: string
rating?: number
}
const {
title,
description = 'No description available.',
rating = 0
} = defineProps<Props>()
const ratingLabel = computed(() => {
return `${rating}/5`
})
</script>
<template>
<article>
<h2>{{ title }}</h2>
<p>
{{ description }}
</p>
<span>
Rating: {{ ratingLabel }}
</span>
</article>
</template>Parent có thể sử dụng:
<ProductCard
title="Vue 3 Guide"
description="A practical guide to Vue."
:rating="5"
/>Hoặc chỉ truyền prop bắt buộc:
<ProductCard title="Vue 3 Guide" />Khi đó:
title
→ "Vue 3 Guide"
description
→ "No description available."
rating
→ 0Một ví dụ với Object prop
Props thường được sử dụng để truyền dữ liệu có cấu trúc.
Ví dụ:
<script setup lang="ts">
interface User {
id: number
name: string
email: string
}
defineProps<{
user: User
}>()
</script>
<template>
<article>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</article>
</template>Parent:
<script setup lang="ts">
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com'
}
</script>
<template>
<UserCard :user="user" />
</template>Ở đây user là một object được truyền xuống component.
Đây là pattern phổ biến khi component cần nhiều thuộc tính liên quan đến cùng một entity.
Tránh truyền quá nhiều props không liên quan
Nếu một component nhận:
name
email
avatar
role
department
location
phone
createdAt
updatedAt
...hãy cân nhắc liệu component có thực sự cần tất cả hay không.
Một component có quá nhiều props thường có API khó hiểu và khó sử dụng.
Thay vì:
<UserCard
:name="user.name"
:email="user.email"
:avatar="user.avatar"
:role="user.role"
:department="user.department"
/>trong một số trường hợp, có thể truyền một object có type rõ ràng:
<UserCard :user="user" />Tuy nhiên, đây không phải quy tắc tuyệt đối.
Props riêng lẻ có thể làm component API rõ ràng hơn nếu component thực sự chỉ cần một vài giá trị.
Điều quan trọng là thiết kế API phù hợp với trách nhiệm của component.
Props không phải là global state
Props phù hợp khi dữ liệu có mối quan hệ rõ ràng giữa parent và child.
Không nên biến props thành cách truyền mọi loại state trong toàn bộ ứng dụng.
Nếu dữ liệu cần được nhiều phần độc lập của ứng dụng truy cập, có thể cân nhắc:
- composable.
- provide/inject.
- state management.
- Pinia.
- các abstraction phù hợp với kiến trúc ứng dụng.
Props nên được sử dụng cho mối quan hệ component rõ ràng:
Parent
↓
ChildNhững lỗi thường gặp khi sử dụng props
1. Quên : khi truyền Number
Sai:
<ProductCard rating="5" />Nếu prop yêu cầu Number, đây là String.
Đúng:
<ProductCard :rating="5" />2. Mutate prop trực tiếp
Sai:
props.title = 'New title'Props là readonly.
3. Mutate nested Object hoặc Array mà không chủ ý
Ví dụ:
props.user.name = 'Bob'có thể ảnh hưởng đến state mà parent đang sở hữu.
Hãy thiết kế luồng cập nhật rõ ràng.
4. Copy prop thành state mà không hiểu trade-off
Ví dụ:
const localValue = ref(props.value)localValue sẽ không tự động đồng bộ với những thay đổi sau đó của props.value.
Cách này phù hợp nếu bạn cố ý sử dụng prop như initial value.
Nếu cần dữ liệu luôn đồng bộ, hãy sử dụng reactive dependency hoặc một pattern component phù hợp hơn.
5. Destructure props mà quên vấn đề reactivity
Trong Vue 3.5+, destructured props được compiler hỗ trợ reactive trong <script setup>.
Tuy nhiên, khi truyền chúng vào watch() hoặc composable, hãy chú ý giữ reactive source bằng getter:
watch(
() => userId,
() => {
// ...
}
)6. Dùng quá nhiều props
Một component nhận quá nhiều props có thể là dấu hiệu component đang có quá nhiều trách nhiệm.
Khi đó nên xem lại component boundaries và API design.
Checklist khi thiết kế props
Trước khi thêm một prop, hãy kiểm tra:
- Component có thực sự cần dữ liệu này không?
- Prop này là required hay optional?
- Type của nó là gì?
- Có cần default value không?
- Có cần runtime validation không?
- Nếu là Object hoặc Array, component con có cần thay đổi dữ liệu không?
- Nếu child cần yêu cầu parent thay đổi state, có nên dùng event không?
- Nếu cần hai chiều,
v-modelcó phù hợp không? - Prop có nên là một giá trị đơn hay một object có cấu trúc?
- Tên prop có rõ ràng không?
- API của component có đang trở nên quá lớn không?
Tóm tắt
Props là cơ chế cốt lõi để truyền dữ liệu từ component cha xuống component con trong Vue.
Luồng cơ bản:
Parent
↓
Props
↓
ChildTrong Vue 3 hiện đại, với <script setup> và TypeScript, cách phổ biến là:
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const {
title,
count = 0
} = defineProps<Props>()
</script>Khi truyền dữ liệu:
<MyComponent
title="Hello"
:count="5"
/>Hãy nhớ những nguyên tắc quan trọng nhất:
1. Props đi xuống
Parent → Child2. Props là readonly
Không mutate trực tiếp prop trong component con.
3. Dùng : khi truyền JavaScript expression
:count="5"thay vì:
count="5"nếu prop cần Number.
4. TypeScript giúp mô tả component API
interface Props {
title: string
count?: number
}5. Dùng default value cho optional props khi cần
const {
count = 0
} = defineProps<Props>()6. Dùng computed() cho derived state
const fullName = computed(() => {
return `${firstName} ${lastName}`
})7. Không dùng props như local mutable state
Nếu cần state riêng, tạo state riêng:
const localValue = ref(props.value)và chỉ làm điều này khi bạn thực sự muốn dùng prop như giá trị khởi tạo.
8. Child → Parent nên dùng emits
Props
Parent → Child
Emits
Child → Parent9. Hai chiều nên cân nhắc v-model
<CustomInput v-model="value" />10. Props là một phần của component API
Một component tốt không chỉ hoạt động đúng mà còn có API rõ ràng:
Props
↓
Input
Events
↓
Output
v-model
↓
Two-way component stateKhi hiểu rõ props, bạn đã nắm được một trong những nền tảng quan trọng nhất của component architecture trong Vue 3.