iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
自我挑戰組

程式碼 AI 寫,我負責看懂:30 天拆解 React × TypeScript系列 第 22

Day 22|Props、State 與資料流:重新看懂資料怎麼流動

  • 分享至 

  • xImage
  •  

Component 拆開後,資料放在哪裡

今天再繼續往下看,了解收藏資料到底該放在哪個 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,卻沒有全部放在一起,那就繼續慢慢往下看為什麼會這樣安排。

先看哪些 Component 需要這份資料

items 不只一個地方在用:

  • PageHeader 要算收藏數量
  • CollectionList 要顯示整份清單
  • CollectionItem 要顯示單筆作品
  • App 的函式要新增、修改、刪除資料

如果把 items 放在 CollectionList 裡,PageHeader 就拿不到數量;放在 PageHeader 也怪,它只負責顯示標題跟數字。

這幾個共同的上層是 App

App
├─ PageHeader
├─ AddItemForm
└─ CollectionList
   └─ CollectionItem

所以把 items 放在 App,再往下傳,感覺比較合理。

React 官方把這種做法叫「提升 State」:好幾個 Component 需要共用同一份 State 時,就搬到它們共同的上層去。
React 官方文件:在 Component 之間共享 State

資料透過 Props 往下傳

Day 07 認識 Props 時知道它能把資料傳給 Component,這次是換個專案再看一次資料是怎麼一路動的。

App 先把總數傳給 PageHeader

<PageHeader totalCount={items.length} />

再把 items 傳給 CollectionList

<CollectionList
  items={items}
  onStatusChange={updateStatus}
  onDelete={deleteItem}
/>

CollectionListmap() 逐一取出每筆作品,再把單筆 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 常說的單向資料流,聽起來很專業,拆開來看其實就是「東西一層一層遞下去」而已。

內層 Component 修改資料的方式

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 丟下去,內層要用時再呼叫。有點像上層給了一支遙控器,內層只負責按按鈕。

表單 State 留在 AddItemForm 的原因

主要資料放在 App,但 titletypestatus 卻沒有一起搬上去。一開始也覺得統一放同個地方比較乾淨,後來才發現不是這樣。

因為這些資料目前只有 AddItemForm 自己在用:

  • title:輸入框的文字
  • type:選漫畫還是動畫
  • status:選的觀看狀態

其他 Component 不需要知道輸入到一半的內容,留在 AddItemForm 就好,不用特地搬家。

送出表單時,AddItemForm 才把整理好的資料交給 App

onAdd({ title: trimmedTitle, type, status })

再由 App 建立新項目、更新 items

setItems((currentItems) => [newItem, ...currentItems])

所以 State 不是什麼都要往上塞,比較合理的做法是:

  • 只有自己需要的資料,留在自己身上
  • 好幾個 Component 都要用的,才放到共同上層

什麼都往 App 堆,反而會讓它管一堆只跟個別 Component 有關的細節,變得又肥又雜。

收藏數量沒有另外建立 State

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:存進瀏覽器,下次打開還能繼續用

網頁一開啟,先靠 loadItemslocalStorage 裡的資料拿出來當初始值,之後 items 一變,再存回 localStorage

重新看懂目前的資料流

整理起來大概是:

AddItemForm
輸入並送出作品
       ↓ onAdd

App
保存及更新 items
├─ PageHeader:顯示收藏數量
└─ CollectionList:顯示整份清單
   └─ CollectionItem:顯示單筆作品

修改或刪除時,CollectionItem 呼叫上層的函式,App 更新 items,新資料再透過 Props 傳下來,畫面就跟著動了。

前面分開學 Props、State、事件處理跟 State 更新時,感覺比較像各學各的。這次放在一起看,能看出它們其實一直在一起合作:

  • State 保存資料
  • Props 把資料跟函式傳下去
  • 事件發生後呼叫函式
  • 更新 State 後,新資料再往下傳一輪

今日回顧

  • 好幾個 Component 都需要的 State,放在共同上層
  • 只有單一 Component 需要的,留在自己身上
  • items 放在 App,因為好幾個 Component 都要用
  • 表單 State 留在 AddItemForm,其他 Component 不需要輸入中的內容
  • 資料從上層透過 Props 往下傳
  • 內層可以呼叫上層傳下來的函式,請上層更新 State
  • 能算出來的內容,不用另外開一份 State
  • localStorage 負責保存,畫面實際用的還是 items

分開了解 Props 和 State 時,能知道它們各自能做什麼,但是不清楚放進專案後要怎麼配合。這次直接跟著 items 走一遍,慢慢看懂資料放在哪裡,以及它會如何影響整個專案傳遞與更新資料的方式。

下篇見!


上一篇
Day 21|AI 幫我拆 Component
系列文
程式碼 AI 寫,我負責看懂:30 天拆解 React × TypeScript22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言