Cách sử dụng Props trong Vue 3


Tuan Duc Tran

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 nameage 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()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 component

Ví 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ả:

Alice

Props 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

Grandchild

Mộ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

Child

Giá 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:

5

Nế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:

5

Quy 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 → number

TypeScript 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: string

Trong khi description là optional:

description?: string

Do đó:

<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 = 0

Cá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: Number

Như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 count

Sau 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.initialCount

Nế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.size

Mà 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:

firstName

Hoặc:

lastName

Thì 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 → Child

Nhưng khi cần giao tiếp theo chiều ngược lại:

Child → Parent

thông thường bạn sẽ sử dụng:

emit

Ví 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 → Parent

Props 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

Parent

Vì 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.name

Trong <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
→ 0

Mộ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

Child

Nhữ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-model có 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

Child

Trong 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 → Child

2. 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 → Parent

9. 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 state

Khi 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.