今天再繼續往下看,了解收藏資料到底該放在哪個 Component。
目前主要的收藏資料放在 App.tsx:
const [items, setItems] = useState<CollectionEntry[]>(loadItems)
但表單裡的作品名稱、類型與觀看狀態,卻放在 AddItemForm.tsx:
const [title, setTitle] = useState('')
const [type, setType] = useState<MediaType>('manga')
const [status, setStatus] = useState<WatchStatus>('want')
同樣都是 State,卻沒有全部放在一起,那就繼續慢慢往下看為什麼會這樣安排。
items 不只一個地方在用:
PageHeader 要算收藏數量CollectionList 要顯示整份清單CollectionItem 要顯示單筆作品App 的函式要新增、修改、刪除資料如果把 items 放在 CollectionList 裡,PageHeader 就拿不到數量;放在 PageHeader 也怪,它只負責顯示標題跟數字。
這幾個共同的上層是 App:
App
├─ PageHeader
├─ AddItemForm
└─ CollectionList
└─ CollectionItem
所以把 items 放在 App,再往下傳,感覺比較合理。
React 官方把這種做法叫「提升 State」:好幾個 Component 需要共用同一份 State 時,就搬到它們共同的上層去。
React 官方文件:在 Component 之間共享 State
Day 07 認識 Props 時知道它能把資料傳給 Component,這次是換個專案再看一次資料是怎麼一路動的。
App 先把總數傳給 PageHeader:
<PageHeader totalCount={items.length} />
再把 items 傳給 CollectionList:
<CollectionList
items={items}
onStatusChange={updateStatus}
onDelete={deleteItem}
/>
CollectionList 用 map() 逐一取出每筆作品,再把單筆 item 傳給 CollectionItem:
{items.map((item) => (
<CollectionItem
key={item.id}
item={item}
onStatusChange={(status) =>
onStatusChange(item.id, status)
}
onDelete={() => onDelete(item.id)}
/>
))}
路線大概長這樣:
App 的 items
↓
CollectionList 的 items
↓
CollectionItem 的 item
這就是 React 常說的單向資料流,聽起來很專業,拆開來看其實就是「東西一層一層遞下去」而已。
CollectionItem 會顯示作品資料,但真正保存 items 的還是 App。
所以 CollectionItem 不會自己改 items,而是呼叫上層傳下來的函式:
onDelete={() => onDelete(item.id)}
按下刪除後,最後就會執行 App 裡的 deleteItem():
function deleteItem(id: string) {
setItems((currentItems) =>
currentItems.filter((item) => item.id !== id),
)
}
修改狀態也是同一套流程:
CollectionItem 修改狀態
↓
呼叫 onStatusChange
↓
App 執行 updateStatus()
↓
setItems() 更新資料
↓
新的 items 再往下傳
內層不是完全動不了上層的 State,而是上層先把函式透過 Props 丟下去,內層要用時再呼叫。有點像上層給了一支遙控器,內層只負責按按鈕。
AddItemForm 的原因主要資料放在 App,但 title、type、status 卻沒有一起搬上去。一開始也覺得統一放同個地方比較乾淨,後來才發現不是這樣。
因為這些資料目前只有 AddItemForm 自己在用:
title:輸入框的文字type:選漫畫還是動畫status:選的觀看狀態其他 Component 不需要知道輸入到一半的內容,留在 AddItemForm 就好,不用特地搬家。
送出表單時,AddItemForm 才把整理好的資料交給 App:
onAdd({ title: trimmedTitle, type, status })
再由 App 建立新項目、更新 items:
setItems((currentItems) => [newItem, ...currentItems])
所以 State 不是什麼都要往上塞,比較合理的做法是:
什麼都往 App 堆,反而會讓它管一堆只跟個別 Component 有關的細節,變得又肥又雜。
PageHeader 顯示的數量是這樣來的:
<PageHeader totalCount={items.length} />
沒有另外弄一個 totalCount State,因為直接從 items.length 算就好。
如果兩份都存,每次新增或刪除都要記得一起更新,漏掉一份,清單跟數字就對不上。
就是 useEffect() 那篇提過的:資料能從現有 Props 或 State 算出來,通常就不用另外開一份。
localStorage 裡的資料算放在哪裡專案會從 localStorage 讀取資料,當作 items 的初始值:
const [items, setItems] = useState<CollectionEntry[]>(loadItems)
items 改變後,再透過 useEffect() 存回去:
useEffect(() => {
saveItems(items)
}, [items])
localStorage 是讓重開網頁後收藏資料還在,不會整個消失。但畫面實際用的還是 State 裡的 items。
items:畫面當下真正在用的資料localStorage:存進瀏覽器,下次打開還能繼續用網頁一開啟,先靠 loadItems 把 localStorage 裡的資料拿出來當初始值,之後 items 一變,再存回 localStorage。
整理起來大概是:
AddItemForm
輸入並送出作品
↓ onAdd
App
保存及更新 items
├─ PageHeader:顯示收藏數量
└─ CollectionList:顯示整份清單
└─ CollectionItem:顯示單筆作品
修改或刪除時,CollectionItem 呼叫上層的函式,App 更新 items,新資料再透過 Props 傳下來,畫面就跟著動了。
前面分開學 Props、State、事件處理跟 State 更新時,感覺比較像各學各的。這次放在一起看,能看出它們其實一直在一起合作:
items 放在 App,因為好幾個 Component 都要用AddItemForm,其他 Component 不需要輸入中的內容localStorage 負責保存,畫面實際用的還是 items
分開了解 Props 和 State 時,能知道它們各自能做什麼,但是不清楚放進專案後要怎麼配合。這次直接跟著 items 走一遍,慢慢看懂資料放在哪裡,以及它會如何影響整個專案傳遞與更新資料的方式。
下篇見!