上一篇把備忘錄搬進 React 後,我們開始看到:
const [memos, setMemos] = useState<string[]>([]);
也知道大概的流程是:
使用者操作
↓
更新 State
↓
React Render
↓
UI 跟著改變
但實際開始看 React 專案後,很快又會遇到另一個問題:
<Component data={data} />
這個 data 又是什麼?
有時候資料放在 useState:
const [data, setData] = useState([]);
有時候卻從 Component 上面傳進來:
<List data={data} />
以前我很容易把它們全部當成:
「反正都是資料。」
但真正開始拆 Component 後,我才慢慢理解:
State 和 Props 最大的差別之一,是這份資料「由誰管理」。
先從上一篇的備忘錄開始:
function MemoApp() {
const [text, setText] = useState("");
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
這裡的:
text
就是 State。
可以先把它理解成:
這個 Component 目前需要記住,而且改變後會影響畫面的資料。
流程:
使用者輸入
↓
onChange
↓
setText()
↓
text 更新
↓
React Render
↓
input 顯示新的 text
所以:
const [text, setText] = useState("");
可以拆成:
text
→ 現在的資料
setText
→ 更新這份資料的方法
接下來假設頁面越來越大。
我們不想把所有東西都寫在:
MemoApp
所以把輸入區拆成另一個 Component:
function MemoInput() {
return ...
}
但問題來了。
原本的 text 在:
MemoApp
裡面。
MemoInput 要怎麼知道現在輸入了什麼?
這時候就可以從 Parent Component 傳下去:
function MemoApp() {
const [text, setText] = useState("");
return (
<MemoInput text={text} />
);
}
MemoInput 再接:
function MemoInput({ text }: { text: string }) {
return (
<input value={text} />
);
}
這裡的:
text
對 MemoInput 來說,就是 Props。
資料流變成:
MemoApp
State
text = "買牛奶"
↓ Props
MemoInput
text = "買牛奶"
↓ Render
<input value="買牛奶" />
所以可以先簡單記:
State
→ Component 自己管理的資料
Props
→ Parent Component 傳進來的資料
這裡就是我以前最容易混亂的地方。
既然:
text
是 Parent 的 State:
function MemoApp() {
const [text, setText] = useState("");
}
那 Child 收到 Props 後,可以直接改嗎?
例如:
function MemoInput({ text }) {
text = "新的內容";
}
不應該這樣做。
Props 對 Child Component 來說,可以先理解成:
Parent 傳給我的輸入資料,我不直接修改它。
如果 Child 希望「通知 Parent 修改」,Parent 可以把更新的方法一起傳下去。
例如:
function MemoApp() {
const [text, setText] = useState("");
return (
<MemoInput
text={text}
onTextChange={setText}
/>
);
}
Child:
type MemoInputProps = {
text: string;
onTextChange: (value: string) => void;
};
function MemoInput({
text,
onTextChange,
}: MemoInputProps) {
return (
<input
value={text}
onChange={e =>
onTextChange(e.target.value)
}
/>
);
}
現在資料流就完整了。
MemoApp
│
│ State: text
│
├──── text ────────────┐
│ ↓
│ MemoInput
│ │
│ 使用者輸入
│ │
│ ↓
│ onTextChange()
│ │
└─────────────────────┘
↓
setText()
↓
State 更新
↓
React Render
這也是 React 裡很常看到的模式:
資料往下傳,事件往上通知。
假設 Parent 有:
const [text, setText] = useState("買牛奶");
Child 又自己寫:
const [text, setText] = useState("買牛奶");
現在其實變成兩份不同的資料:
Parent text
→ "買牛奶"
Child text
→ "買牛奶"
一開始看起來一模一樣。
但如果 Parent 更新成:
"買咖啡"
Child 自己那份 State 不一定會跟著變。
就會開始出現:
到底哪一份才是真的?
這其實又回到前一篇提到的:
Source of Truth 到底在哪裡?
如果多個 Component 都需要同一份資料,通常會先思考:
這份資料應該由哪一層負責管理?
而不是每個 Component 都複製一份自己的 State。
這也是我後來在實際專案拆頁面時,才真正開始理解 Props 的地方。
假設原本整個頁面都寫在同一個 Component:
DetailPage
│
├─ 基本資料
├─ 商品資料
├─ 文件資料
└─ 其他資訊
所有資料都在同一個地方時,其實不用特別思考「資料怎麼傳」。
因為大家都在同一個 Component 裡。
但當頁面開始拆成:
DetailPage
│
├─ BasicInfo
├─ ProductInfo
├─ DocumentInfo
└─ OtherInfo
問題馬上出現:
BasicInfo 需要哪些資料?
ProductInfo 需要哪些資料?
DocumentInfo 又需要哪些資料?
這時候就不能只是:
「把程式碼剪出去就好了。」
還要開始整理資料流。
例如:
DetailPage
↓
取得 API Response
↓
保存需要的資料
↓
透過 Props
├─ BasicInfo
├─ ProductInfo
└─ DocumentInfo
Component 拆分之後,我反而會更容易看到:
每個 Component 到底依賴哪些資料?
這也是 Props 對我來說開始真正有意義的時候。
先不用背太正式的定義。
我目前比較喜歡這樣理解:
| State | Props | |
|---|---|---|
| 資料從哪來 | Component 自己管理 | Parent 傳進來 |
| 誰負責更新 | 擁有 State 的 Component | Parent |
| Child 可以直接修改嗎 | 使用 setter 更新自己的 State | 不直接修改 Props |
| 改變後 | 可能觸發重新 Render | Parent 傳入新 Props 後 Child 重新 Render |
如果再濃縮一點:
State
→ 我管理的資料
Props
→ 別人傳給我的資料
但真正重要的不是背這兩句。
而是看到一份資料時開始問:
這份資料在哪裡建立?
↓
誰負責管理?
↓
誰需要使用?
↓
誰可以修改?
↓
修改後要通知誰?
前幾天我們一路從:
資料在哪?
走到了:
資料怎麼讓 UI 改變?
今天又多了一層:
資料由誰管理?
↓
哪些 Component 需要它?
↓
要透過 Props 傳給誰?
所以 React Component 之間的關係,可以先想成:
Parent
│
│ State
│
↓
Props
│
↓
Child
│
│ Event / Callback
│
↑
通知 Parent
當這條線開始看得懂之後,很多原本看起來很亂的 React 程式碼,就能開始問:
這個 Component 是資料的主人,還是只是使用別人傳進來的資料?
下一篇就可以正式進到一個我實際工作後很常遇到的問題:
一個頁面到底什麼時候該拆 Component?拆得越多真的越好嗎?