在上一篇的認識 JSX 中有出現這麼段程式碼:
// App.tsx
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
當時只知道 <TodoList /> 是自己建立的 Component,但標籤裡的 todos、onToggle 和 onDelete 又是什麼呢?
它們都是傳給 TodoList 的 Props。
Props 是 Properties 的縮寫,可以讓一個 Component 接收外部提供的資料。React Component 會使用 Props 彼此溝通,外層 Component 可以透過 Props 將資訊傳給內層 Component。
React 官方文件:將 Props 傳遞給 Component
在這段程式碼中,App 使用了 TodoList,並透過 Props 提供三個值:
todos:待辦事項陣列onToggle:切換完成狀態的函式onDelete:刪除待辦事項的函式因此,可以先把 Props 理解成:
使用 Component 時,從外部提供給它的資料。
在 React 中,可以透過 JSX 標籤上的屬性傳入 Props。
例如:
<TodoList todos={todos} />
這表示 TodoList 會收到一個名為 todos 的 Prop,而它的值是大括號裡的 todos 變數。
如果要一次傳入多個 Props,就可以繼續寫在 Component 標籤上:
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
這種寫法看起來有點像在 HTML 標籤上加入屬性,但 React Props 的值不只能是文字,也可以是各種 JavaScript 值。
例如:
<Component
title="Todo List"
count={3}
completed={false}
todos={todos}
onDelete={deleteTodo}
/>
這些 Props 分別傳入了:
如果傳入的是固定字串,可以直接寫成:
title="Todo List"
如果傳入的是 JavaScript 運算式,則要使用大括號:
count={3}
completed={false}
todos={todos}
onDelete={deleteTodo}
因此,Props 並不只是一組寫在 Component 標籤上的文字,而是可以將各種 JavaScript 值提供給另一個 Component。
外層的 App 傳入 Props 後,TodoList 需要透過 function 的參數接收。
目前專案中的 TodoList.tsx 是這樣寫:
function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
// ...
}
這裡的 : TodoListProps 是 TypeScript 的型別標註,用來告訴 TypeScript「這個 Component 會收到哪些 Props、每個 Props 是什麼型別」。TodoListProps 本身是另外定義好的 interface(這邊目前先不深入解釋,打算留到之後的某一篇幅再說。)
現在只需要知道:拿掉 : TodoListProps 之後,剩下的 { todos, onToggle, onDelete } 才是這篇文章真正要講的物件解構部分。
為什麼 Props 會出現在 function 的參數裡?
因為 React Component 本身就是一個 function。React Component function 會接收一個參數,也就是包含所有傳入資料的 Props 物件。
如果不使用目前專案中的物件解構,也可以直接透過 props 接收:
function TodoList(props) {
console.log(props.todos)
console.log(props.onToggle)
console.log(props.onDelete)
}
這裡的 props 可以想成一個物件:
const props = {
todos: todos,
onToggle: toggleTodo,
onDelete: deleteTodo,
}
因此,要取得其中的 todos,可以使用:
props.todos
要取得另外兩個 Props,則可以使用:
props.onToggle
props.onDelete
這就是 React Component 接收 Props 的基本方式。
props.?前面說這麼多但卻沒看到有寫props.todos對叭
因為專案中的 TodoList 並不是透過 props.todos 取得資料,而是直接使用function 的參數使用了 JavaScript 的物件解構。
todos
如果不使用物件解構,會寫成:
function TodoList(props) {
console.log(props.todos)
console.log(props.onToggle)
console.log(props.onDelete)
}
使用物件解構後,可以直接從 Props 物件中取出需要的內容:
function TodoList({ todos, onToggle, onDelete }) {
console.log(todos)
console.log(onToggle)
console.log(onDelete)
}
所以,下面兩種寫法取得的是相同的資料:
props.todos
todos
使用物件解構後,就不需要在每次使用時都寫上 props.。
回頭看目前專案的寫法:
function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
if (todos.length === 0) {
return <p className="empty">目前沒有待辦事項,新增一筆看看吧!</p>
}
// ...
}
TodoList 可以直接使用收到的 todos,取得待辦事項的數量。
<TodoList todos={todos} />
第一次看到這段時只覺得有看沒有懂,越看越霧煞煞,為什麼左右兩邊都是 todos??
雖然名稱相同,但它們代表的角度不同:
todos={todos}
↑ ↑
Prop 名稱 App 裡的變數
左邊的 todos 是 Prop 的名稱,也就是 TodoList 接收時使用的名稱;右邊大括號裡的 todos,則是 App 裡目前要傳入的 JavaScript 變數。
這段程式碼可以理解成:
TodoList的todosProp =App裡的todos變數
左右兩邊不一定要使用相同名稱,只是目前專案剛好取了相同的名字,所以才容易讓人誤會兩者是同一件事。
假設 App 裡的變數叫做 todoItems:
const todoItems = []
使用 TodoList 時可以寫成:
<TodoList todos={todoItems} />
這代表:
TodoList的todosProp =App裡的todoItems變數
TodoList 接收時仍然使用 todos:
function TodoList({ todos }) {
console.log(todos)
}
Prop 的名稱是由開發者決定的,但傳入與接收時使用的 Prop 名稱必須互相對應。
如果傳入時改成:
<TodoList items={todos} />
接收時就要使用 items:
function TodoList({ items }) {
console.log(items)
}
React 會根據 Prop 名稱把資料放進 Props 物件,所以傳入與接收時使用的 Prop 名稱必須一致,與外層變數的名稱是否相同無關。
這邊專案中使用 todos,是因為這個名稱可以直接表達它是一組待辦事項。
另外兩個 Props 使用 onToggle 和 onDelete,也可以看出它們提供的是與切換、刪除有關的函式。
清楚的 Props 名稱,可以讓人直接從 Component 的使用方式,大致看懂它需要哪些資料。
App 將 Props 傳給 TodoList:
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
TodoList 接收後,也可以將收到的內容繼續傳給 TodoItem:
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
在這段程式碼中,TodoList 傳給 TodoItem 三個 Props:
todo
onToggle
onDelete
接著,TodoItem 再透過 function 的參數接收:
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
// ...
}
專案裡的 Props 傳遞可以看成:
App
↓ Props
TodoList
↓ Props
TodoItem
每個 Component 只要透過 function 的參數接收,就可以使用外部提供的資料。
程式碼中的 key 是 React 用來辨認清單項目的特殊屬性,不會像一般 Props 一樣傳給 TodoItem,所以 TodoItem 接收到的是 todo、onToggle 和 onDelete。
Props 是外部提供給 Component 的資料,接收 Props 的 Component 不應該直接修改它。
例如,TodoList 收到 todos:
function TodoList({ todos }) {
// ...
}
不應該直接在 TodoList 裡修改這個陣列:
todos.push(newTodo)
可以將 Props 理解成 Component 在每次 Render 時收到的一份唯讀資料。Component 可以讀取並使用它,但不應該直接修改它。
例如,可以讀取陣列長度:
todos.length
也可以讀取其中一筆資料:
todo.text
Props 本身不會自己改變——它更像是外層在某個時間點交給 Component 的一份快照。當外層傳入新的值時,Component 不會「修改」手上這份快照,而是在下一次 Render 收到一份新的快照。
React 官方文件將 Props 形容為每次 Render 收到的唯讀快照,因此收到 Props 的 Component 不應該直接改變它。React 官方文件:Props 如何隨時間改變 (這樣的限制可以讓資料的來源與管理權責更加清楚。)
回頭看看 Vue Todo List 專案,App.vue 傳入 todos 的寫法是:
<TodoList :todos="todos" />
React Todo List 專案則是:
<TodoList todos={todos} />
這兩段程式碼做的事情很接近,都是將外層的 todos 變數傳給 TodoList,主要差別是語法。
接收 Props 的方式也不同。
Vue 的 TodoList.vue 使用 defineProps():
<script setup>
defineProps({
todos: {
type: Array,
required: true,
},
})
</script>
React 的 TodoList 則透過 function 的參數接收:
function TodoList({ todos }) {
// ...
}
將兩者放在一起比較:
| Vue | React |
|---|---|
使用 :todos="todos" 傳入變數 |
使用 todos={todos} 傳入變數 |
使用 defineProps() 宣告 Props |
使用 function 參數接收 Props |
在 Template 中使用 todos |
在 Component function 中使用 todos |
兩者的共通點是:Props 都不應該被接收方直接修改,這一點無論在 Vue 或 React 都一樣,是資料單向流動的基本原則。
所以 Vue 與 React 的 Props 在概念上很相似,都是讓 Component 接收外部資料。
這部分對我來說,應該需要適應的不是 Props 的用途,而是 React 不使用 defineProps(),直接將 Props 交給 Component function。
如果只看用途,它們確實很接近:
都是讓一個 Component 接收外部提供的資料。
不過,因為 Vue 和 React 建立 Component 的方式不同,所以 Props 的語法也不一樣。
Vue 會先使用 defineProps() 宣告:
defineProps({
todos: {
type: Array,
required: true,
},
})
React 則直接從 function 的參數接收:
function TodoList({ todos }) {
// ...
}
React Props 目前我先簡單理解成:
使用 Component 時,寫在 JSX 標籤上並傳給 Component function 的資料。
今天從 React Todo List 專案認識了 Props:
props.todos 取得其中的資料回頭再看原本覺得陌生的程式碼:
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
現在知道這段程式碼是在使用 TodoList 時,將三個 Props 傳給它。
而 TodoList 透過 function 的參數接收:
function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
// ...
}
Vue 有 Props,React 也有 Props。兩者想解決的問題確實很像,只是到了 React,Props 會直接進入 Component function,成為 Component 描述畫面時可以使用的資料。
這篇先這樣,不小心打的落落長
下一篇 Day 08 預計來了解一下 React 的 map() 下篇見!