iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0
自我挑戰組

React 開發者的 TypeScript 探索之旅系列 第 25

【 Day 25 】重構提示訊息

  • 分享至 

  • xImage
  •  

本系列文章 GitHub

Day23,我們提到了像 messageDetails 這樣只涉及少數元件的狀態,使用局部管理可以避免不必要的全域依賴並保持程式碼的簡潔。
但聰明的你應該已經發現了,若新增功能的成功與失敗有提示訊息,那麼編輯成功與失敗也應該要有提示訊息,以保持 UI 的一致性。

在最初設計時,沒有顧慮到這一點,若要依然使用局部狀態管理來處理提示訊息,狀態會經過 App => TodoList => Todo 這樣的路徑傳遞。這不但會讓程式碼變得複雜,還可能導致重複的邏輯。因此,決定使用 useReduceruseContext 來重構提示訊息管理,讓這部分邏輯統一放在全域狀態中處理。

若你對於 useReduceruseContext 已經非常熟悉,可以直接跳過此篇,因為我們的 Todo List 基本功能已經完成,後續的幾篇不會再與 Todo List 有關,若你對於 useReduceruseContext 並不是那麼的熟悉,歡迎跟著我一起往下實作。


TodoContext.tsx

首先,我們要修改 MessageDetails 型別。除了 visible 每次都會更新,messagemode 不一定每次都需要,因此我們將這兩個屬性設為可選:

export type MessageDetails = {
  visible: boolean
  message?: string
  mode?: 'error' | 'success'
}

然後更新 ActionType

type ActionType =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'DELETE_TODO'; payload: number }
  | { type: 'EDIT_TODO_TITLE'; payload: { id: number; title: string } }
  | { type: 'TOGGLE_TODO_ISFINISHED'; payload: number }
  | { type: 'MESSAGE'; payload: MessageDetails }

我們需要為提示訊息定義狀態型別和初始狀態:

// 定義狀態
type StateType = {
  todos: TodoItem[]
  messageDetail: MessageDetails
}

// 初始狀態
const initialState: StateType = {
  todos: [],
  messageDetail: {
    visible: false,
    message: '',
    mode: 'error',
  },
}

然後在 reducer 中加入處理提示訊息的邏輯:

case 'MESSAGE':
  return {
    ...state,
    messageDetail: {
      ...state.messageDetail, // 保留現有的 message 屬性
      ...action.payload, // 只覆蓋傳入的屬性
    },
  }

App.tsx

刪除原來用 useState 定義的局部狀態:

const [messageDetails, setMessageDetails] = useState<MessageDetails>({
  visible: false,
  message: '',
  mode: 'error',
})

更新前的 createTodoHandler 如下:

const createTodoHandler = (title: string) => {
  if (title.trim().length === 0) {
    setMessageDetails({
      visible: true,
      message: 'Input cannot be empty!',
      mode: 'error',
    })
    return   
  }
  dispatch({ type: 'ADD_TODO', payload: title })
  setMessageDetails({
    visible: true,
    message: 'Todo created successfully!',
    mode: 'success',
  })
}

setMessageDetails 替換為 dispatch,重構後:

const createTodoHandler = (title: string) => {
  if (title.trim().length === 0) {
    dispatch({
      type: 'MESSAGE',
      payload: {
        visible: true,
        message: 'Input cannot be empty!',
        mode: 'error',
      },
    })
    return 
  }
  dispatch({ type: 'ADD_TODO', payload: title })
  dispatch({
    type: 'MESSAGE',
    payload: {
      visible: true,
      message: 'Todo created successfully!',
      mode: 'success',
    },
  })
}

我們不再需要將 props 傳給 Message 元件:

<Message />

Message.tsx

移除所有 props,並使用 useContext 來存取全域狀態:

import { useContext, useEffect } from 'react'
import { TodoContext } from '../store/TodoContext'

export default function Message() {
  const { dispatch } = useContext(TodoContext)
  const { visible, message, mode } = useContext(TodoContext).state.messageDetail

  useEffect(() => {
    if (visible) {
      const timer = setTimeout(() => {
        dispatch({ type: 'MESSAGE', payload: { visible: false } })
      }, 3000)

      return () => clearTimeout(timer)
    }
  }, [visible])
  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>
  )
}

Todo.tsx

為編輯標題和勾選操作加入提示訊息:

// 提交新的標題
const submitNewTitle = () => {
  // 如果標題是空的,則不更新
  if (typeof newTitle === 'string' && newTitle.trim().length === 0) {
    setNewTitle(children)
    setIsEditing(false)
    dispatch({
      type: 'MESSAGE',
      payload: {
        visible: true,
        message: 'Title cannot be empty!',
        mode: 'error',
      },
    })
    return
  }
  if (typeof newTitle === 'string' && newTitle.trim().length > 0) {
    dispatch({
      type: 'EDIT_TODO_TITLE',
      payload: { id, title: newTitle },
    })
    dispatch({
      type: 'MESSAGE',
      payload: {
        visible: true,
        message: 'Title updated successfully!',
        mode: 'success',
      },
    })
  }
}

// 處理勾選
const checkboxHandler = () => {
  dispatch({
    type: 'TOGGLE_TODO_ISFINISHED',
    payload: id,
  })
  dispatch({
    type: 'MESSAGE',
    payload: {
      visible: true,
      message: 'Todo updated successfully!',
      mode: 'success',
    },
  })
}

上一篇
【 Day 24 】加入編輯功能
下一篇
【 Day 26 】如何用 TypeScript 建立通用的 Input 元件
系列文
React 開發者的 TypeScript 探索之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言