iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
自我挑戰組

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

Day 23|從輸入資料到加入收藏,拆解表單流程

  • 分享至 

  • xImage
  •  

從輸入框開始跟著資料走

上一篇順著 items 看了一次資料如何在 Component 之間傳遞。今天則把範圍縮小到 AddItemForm.tsx,看看從輸入作品名稱到加入收藏,中間經過了哪些步驟。

目前表單裡有三份 State:

const [title, setTitle] = useState('')
const [type, setType] = useState<MediaType>('manga')
const [status, setStatus] = useState<WatchStatus>('want')

它們分別用來記住作品名稱、作品類型與觀看狀態。

作品名稱的輸入框則是這樣:

<input
  value={title}
  onChange={(event) => setTitle(event.target.value)}
  placeholder="例如:葬送的芙莉蓮"
  required
/>

value={title} 表示輸入框顯示的內容來自 title。使用者輸入文字時,onChange 會取得新的內容,再透過 setTitle() 更新 State。

這種由 State 決定輸入框內容的寫法稱為「受控輸入」,目前可以先理解成:

輸入框顯示什麼內容,由 React State 負責記住。

如果只有 value={title},卻沒有使用 onChange 更新 title,輸入框就會一直收到原本的值,導致文字無法正常輸入。
React 官方文件:使用 State 控制輸入框

下拉選單裡的 as

作品類型與觀看狀態使用 <select>,處理方式和輸入框差不多:

<select
  value={type}
  onChange={(event) =>
    setType(event.target.value as MediaType)
  }
>

value={type} 決定目前選到的項目,選項改變時,再透過 setType() 更新 State。

仔細一看,這裡也出現了 Day 20 曾經覺得陌生的寫法:

event.target.value as MediaType

瀏覽器取得的 event.target.value 會被 TypeScript 看成一般的 string,但專案已經限制 type 只能是:

type MediaType = 'manga' | 'anime'

所以 as MediaType 是在告訴 TypeScript:

這裡取得的字串,可以當成 MediaType 使用。

這種寫法稱為 Type Assertion,也就是「型別斷言」。
不過 as 只是在開發時提供型別資訊,不會真的檢查或轉換資料。如果實際拿到錯誤的內容,它也不會自動修正,所以不能只是為了讓型別錯誤消失就隨意加上 as。觀看狀態的 as WatchStatus 也是相同的用法。
TypeScript 官方文件:Type Assertions

按下「加入收藏」後的處理流程

表單透過 onSubmit 指定送出時要執行的函式:

<form className="add-form" onSubmit={handleSubmit}>

按下「加入收藏」後,就會執行 handleSubmit()

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault()
  const trimmedTitle = title.trim()

  if (!trimmedTitle) return

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

這段程式依序會:

  1. 使用 event.preventDefault() 阻止表單重新整理網頁。
  2. 使用 trim() 移除作品名稱前後的空白。
  3. 如果整理後沒有任何內容,就直接結束函式。
  4. 透過 onAdd 將表單資料交給 App
  5. 呼叫 onAdd 後,使用 setTitle('') 清空作品名稱。

其中的:

React.FormEvent<HTMLFormElement>

是在描述這個 event 來自 HTML 表單的送出事件,讓 TypeScript 知道目前正在處理哪一種事件。

表單資料裡沒有 id

AddItemForm 不會直接更新 items,而是呼叫透過 Props 傳進來的 onAdd

interface AddItemFormProps {
  onAdd: (entry: Omit<CollectionEntry, 'id'>) => void
}

這裡也出現另一個陌生的寫法:

Omit<CollectionEntry, 'id'>

完整的 CollectionEntry 包含:

interface CollectionEntry {
  id: string
  title: string
  type: MediaType
  status: WatchStatus
}

但是表單只需要提供:

title
type
status

id 不需要由使用者填寫,而是交給 App 產生。

Omit 可以先理解成「從原本的型別中拿掉指定欄位」。所以 Omit<CollectionEntry, 'id'> 就是沿用 CollectionEntry 的格式但先拿掉 id ,這樣不需要為表單資料另外寫一份幾乎相同的型別。
TypeScript 官方文件:Omit

App 補上 id 並加入清單

送出表單時,這段程式:

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

會把資料交給 App 裡的 addItem()

function addItem(entry: Omit<CollectionEntry, 'id'>) {
  const newItem: CollectionEntry = {
    ...entry,
    id: crypto.randomUUID(),
  }

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

...entry 會放入表單提供的 titletypestatuscrypto.randomUUID() 則替這筆收藏建立 id

最後 setItems() 建立新的陣列,將新作品放在原本收藏資料的前面。更新完成後,新的 items 再透過 Props 傳給 CollectionList,畫面上就會出現剛才新增的作品。

一筆收藏走過的完整流程

把整個流程整理起來,大概是:

輸入作品名稱、類型與狀態
          ↓
AddItemForm 的 State 記住內容
          ↓
送出表單並執行 handleSubmit()
          ↓
透過 onAdd 把資料交給 App
          ↓
App 補上 id 並更新 items
          ↓
畫面顯示新增的作品

順著資料走過一次才比較能看出它們都在協助完成同一件事:把使用者輸入的內容整理成一筆收藏資料,再加入清單。

今日回顧

  • value 搭配 onChange,讓 State 記住表單目前的內容
  • handleSubmit() 負責整理資料並呼叫 onAdd
  • as 用來告訴 TypeScript 如何看待一份資料的型別
  • Omit<CollectionEntry, 'id'> 表示沿用原本的型別,但先拿掉 id
  • App 會補上 id,再透過 setItems() 將作品加入清單

下篇見!


上一篇
Day 22|Props、State 與資料流:重新看懂資料怎麼流動
下一篇
Day 24|從保存到讀取,拆解 `localStorage` 的資料流程
系列文
程式碼 AI 寫,我負責看懂:30 天拆解 React × TypeScript30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言