目前的 Todo List 在輸入欄位為空白時仍然可以送出。今天,我們要來修正這個問題,並為其新增錯誤提示訊息。此外,為了練習我們在 Day08 提到的 Literal Types
,我們將在 Todo
成功建立時顯示成功提示訊息。
首先,在 components
資料夾中加入檔案 Message.tsx
,並建立 Message
元件。Message
元件將放置在 App.tsx
中,並接收來自 App
的 props
,透過這些 props
來控制顯示與否以及顯示的內容。因此,我們將 type
定義在 App.tsx
並匯出,以便 Message
元件可以共用這個型別。
由於我們的提示訊息只會有「成功」和「失敗」兩種狀態,因此可以使用 Literal Types
來定義 mode
的型別:
export type MessageDetails = {
visible: boolean
message: string
mode: 'error' | 'success'
}
接著,在 App.tsx
中建立狀態,並定義其初始值:
const [MessageDetails, setMessageDetails] = useState<MessageDetails>({
visible: false,
message: '',
mode: 'error',
})
由於 Message
元件會使用 fixed
定位,它可以放置在 <main>
中的任意位置。這裡我們選擇將它放置在頁面底部,並傳入定義好的狀態作為 props
:
...
<main className='w-[500px] h-[100dvh] portrait:w-[90%] flex flex-col'>
<Header image={{ src: logo, alt: 'logo' }}>
<h1>Todo List</h1>
</Header>
<CreateTodo onCreateTodo={createTodoHandler} />
<TodoList todos={todos} onDeleteTodo={deleteTodoHandler} />
<Message
visible={messageDetails.visible}
mode={messageDetails.mode}
message={messageDetails.message}
/>
</main>
...
前往 Message.tsx
並匯入我們在 App.tsx
中定義的 MessageDetails
型別:
import { type MessageDetails } from '../App'
export default function Message({ visible, message, mode }: MessageDetails) {
return (
<div
className={`${mode === 'error' ? 'bg-red-500' : 'bg-green-500'} ${
visible ? 'flex' : 'hidden'
} rounded-[5px] p-[10px] fixed bottom-[20px] left-[20px]`}
>
<p className='text-[20px]'>{message}</p>
</div>
)
}
打開瀏覽器,試著手動更改 App
元件中 messageDetails
的初始值,你應該可以看到像下方截圖中的這樣畫面:
接著,回到 App
元件,在 createTodoHandler
函式中新增提示訊息的相關邏輯,以處理成功和錯誤狀態:
const createTodoHandler = (title: string) => {
if (title.trim().length === 0) {
setMessageDetails({
visible: true,
message: 'Input cannot be empty!',
mode: 'error',
})
return
}
const newTodo: TodoItem = {
id: Math.random(),
title: title,
isFinished: false,
}
setTodos((prevTodos) => [...prevTodos, newTodo])
setMessageDetails({
visible: true,
message: 'Todo created successfully!',
mode: 'success',
})
}
目前提示訊息的顯示功能已經完成,接下來,我們要處理提示訊息的自動隱藏。Message
元件的顯示狀態儲存在 App.tsx
中的 messageDetails
狀態,因此,我們需要將更新狀態的 setMessageDetails
函式傳遞給 Message
元件:
<Message
visible={messageDetails.visible}
mode={messageDetails.mode}
message={messageDetails.message}
onMessageVisible={setMessageDetails}
/>
在 Message
元件中,我們會使用先前在 Day08 提到的合併型別方法加入 onMessageVisible
。為了增強程式碼的可讀性,這裡我們將型別定義獨立提出來,並設置 onMessageVisible
的參數型別為 SetStateAction
,泛型參數則使用我們先前定義的 MessageDetails
:
import { type SetStateAction } from 'react'
type MessageProps = MessageDetails & {
onMessageVisible: (value: SetStateAction<MessageDetails>) => void
}
最後,加入 useEffect
,當 visible
為 true
時,觸發計時器,在三秒後自動隱藏提示訊息,並清除上一次的計時器,避免計時器累加造成記憶體的負擔:
useEffect(() => {
if (visible) {
const timer = setTimeout(() => {
onMessageVisible((prev: MessageDetails) => ({
...prev,
visible: false,
}))
}, 3000)
return () => clearTimeout(timer)
}
}, [visible])
在最一開始,我們提到了使用 Literal Types
定義型別:
export type MessageDetails = {
visible: boolean
message: string
mode: 'error' | 'success'
}
若有一個元件不需要 mode
,而我們在使用 MessageDetails
型別時沒有傳遞 mode
給該元件,那麼 TypeScript 會報錯。在這種情況下,我們可以有兩種解決方法。
第一種寫法是將 mode
設為可選的,並允許其值為 undefined
,這樣元件即使沒有傳遞 mode
也不會報錯:
export type MessageDetails = {
visible: boolean
message: string
mode: 'error' | 'success' | undefined
}
第二種寫法是在 mode
後加上 ?
,這樣就會讓 mode
成為可選的屬性:
export type MessageDetails = {
visible: boolean
message: string
mode?: 'error' | 'success'
}
兩種寫法效果相同,可以根據個人喜好選擇。