上一篇我們把備忘錄搬進 React,開始看到:
const [memos, setMemos] = useState<string[]>([]);
也知道大致流程是:
使用者操作
↓
更新 State
↓
React Render
↓
UI 跟著改變
但開始看真正的 React 專案後,很快又會看到另一種東西:
<ProductList products={products} />
這時候我以前很容易混在一起:
State?
Props?
反正都是資料?
後來才慢慢理解,真正要先問的其實不是:
「它叫 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 顯示新的內容
所以:
const [text, setText] = useState("");
可以先拆成:
text
→ 現在的資料
setText
→ 更新這份資料的方法
假設現在頁面越來越大。
我們不想把所有東西都塞在:
MemoApp
裡面。
所以把輸入區拆成另一個 Component:
function MemoInput() {
return ...
}
但問題來了。
原本的 text 在:
MemoApp
裡。
那 MemoInput 要怎麼知道現在輸入的是什麼?
可以從 Parent 傳下去:
function MemoApp() {
const [text, setText] = useState("");
return (
<MemoInput text={text} />
);
}
然後 Child 接:
type MemoInputProps = {
text: string;
};
function MemoInput({ text }: MemoInputProps) {
return (
<input value={text} />
);
}
這時候:
text
對 MemoInput 來說,就是 Props。
資料流可以畫成:
MemoApp
State
text = "買牛奶"
↓ Props
MemoInput
text = "買牛奶"
↓ Render
<input value="買牛奶" />
所以可以先簡單記:
State
→ Component 自己管理的資料
Props
→ Parent 傳進來的資料
這裡就是我以前最容易卡住的地方。
假設:
text
其實是 Parent 的 State。
那 Child 收到後,可以直接改嗎?
例如:
function MemoInput({ text }: MemoInputProps) {
text = "新的內容";
}
不是這樣處理。
Props 對 Child 來說,可以先理解成:
這是 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)
}
/>
);
}
現在流程就變成:
Parent
│
│ State: text
│
├──── text ─────────────┐
│ ↓
│ Child
│ │
│ 使用者輸入
│ │
│ ↓
│ 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 都要使用同一份資料,就要先想清楚:
誰負責管理?
↓
誰只是使用?
而不是每個地方都自己複製一份 State。
這也是我後來在實際專案裡,才真正開始理解 Props 的地方。
假設一開始整個頁面都寫在同一個 Component:
DetailPage
│
├─ 基本資料
├─ 商品資料
├─ 文件資料
└─ 其他資訊
因為全部都在一起,所以其實不太會感覺到「資料怎麼傳」。
但開始拆 Component 後:
DetailPage
│
├─ BasicInfo
├─ ProductInfo
├─ DocumentInfo
└─ OtherInfo
問題馬上出現:
BasicInfo 需要哪些資料?
ProductInfo 需要哪些資料?
DocumentInfo 又要哪些資料?
這時候就不能只是把 JSX 剪出去。
還要開始整理:
資料在哪裡?
↓
誰擁有?
↓
要傳給誰?
例如:
<ProductInfo
products={detail.products}
currency={detail.currency}
/>
光看 Props,就可以開始知道:
ProductInfo 依賴:
products
currency
所以拆 Component 之後,資料依賴反而會變得比較清楚。
我現在比較喜歡這樣記:
| State | Props | |
|---|---|---|
| 資料從哪來 | Component 自己管理 | Parent 傳進來 |
| 誰負責更新 | 擁有 State 的 Component | 通常由 Parent 決定 |
| Child 能直接改嗎 | 用自己的 setter 更新 | 不直接修改 Props |
| 改變後 | 可能重新 Render | Parent 傳入新值後重新 Render |
如果再縮短:
State
→ 我管理的資料
Props
→ 別人傳給我的資料
但比起背這兩句,更重要的是看到一份資料時開始問:
這份資料在哪裡建立?
↓
誰負責管理?
↓
誰需要使用?
↓
誰可以修改?
↓
修改後要通知誰?
前幾天我們一路在問:
資料在哪?
↓
資料怎麼讓 UI 改變?
今天再多一層:
資料是誰的?
↓
誰負責管理?
↓
哪些 Component 要使用?
所以 Component 之間的關係,可以先記成:
Parent
│
│ State
│
↓
Props
│
↓
Child
│
│ Callback / Event
│
↑
通知 Parent
當這條線開始看得懂之後,很多原本很亂的 React 程式碼,就可以先問:
這個 Component 是資料的主人,還是只是使用別人傳進來的資料?
下一篇就接著進一個我工作後很常遇到的問題:
一個頁面到底什麼時候該拆 Component?拆得越多真的越好嗎?