iT邦幫忙

2022 iThome 鐵人賽

DAY 12
0
Modern Web

React Hook 不求人,建立自己的 Hook Libary系列 第 12

[DAY 12] 自己的Hook自己做!能取消動作的 useEventControl!

  • 分享至 

  • xImage
  •  

前言

繼承上一篇 Never trust user input 的精神,有一種使用者也是操作迅速,彷彿在玩 OSU (如上圖,音樂遊戲),結果不小心按到「刪除」...,然後PM就跑來跟你說客戶覺得產品「很難用」...இдஇ

常見處理方式不外乎就是用 comfirm 來 double check,或是使用 prompt 要求使用者輸入特定文字,例如在 Github 上要刪除專案時,就會要求你輸入特定字串才能進一步刪除。

另一種就是能夠取消的 request,當使用者按下按鈕時,會告知處理中,並附上一顆取消按鈕來讓使用者取消,Google 雲端硬碟上傳時也很常見到。

不論是哪種作法,在設計規劃時,可以依照這個動作的嚴重性來搭配對應的處理方式,本篇要製作的就是最後提及的—可取消功能。

功能與情境

當使用者觸發事件時,會進入 pending 的狀態,並提供可以取消的動作進一步取消剛剛觸發的事件。

理想是:

  • 使用者可以主動觸發
  • 使用者可以主動取消

你可能比較想先看DEMO,在這裡

開始!

使用 alert 當作範例,並包裝成如下,現在只要點擊 Button 就會跳出 alert,這是預期之內的動作。

function showAlert(msg) {
  alert("Message:" + msg)
}

function Example() {
  return (
    <Button onClick={() => showAlert("I am alert!")}>
      TRIGGER
    </Button>
  )
}

建立 useEventControl

與前一篇的 useDebounce 雷同,會使用到 setTimeout,但差別在於我們要主動控制而不依賴其他 state 來觸發,因此期望回傳:

function useEventControl(cb, delay) { 
  // 接受一個cb: callback, delay: 延遲時間(ms)
  
  return [startEvent, isPending, cancelEvent]
  //分別是 觸發 / 執行狀態 / 取消 
}

當然也會有 useEffect,使用者會藉由「操控」 isPending 來觸發執行,useEffect 則有 isPending 當作 deps,改變時也會進一步觸發裡面的內容:

const timeoutRef = useRef(null)
const [isPending, setIsPending] = useState(false)

//開始
const startEvent () => {
  if (timeoutRef.current) {
    clearTimeout(timeoutRef.current)
  }
  setIsPending(true)
}

//取消
const cancelEvent = () => {
  if (timeoutRef.current) {
    clearTimeout(timeoutRef.current)
  }
  setIsPending(false)
}

useEffect(() => {
  
  if (isPending) {
    timeoutRef.current = setTimeout(() => {
      cb()
      setIsPending(false)
    }, delay)
  }

  return () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }
  }
}, [isPending])
  • useEffect 在 first-render 會先行執行一次,相比前一篇用 useRef,這邊直接使用 isPending
  • 開始與取消都會先行 clear 前一次的 setTimeout
  • 由於 setTimoue 要能被取消,用 useRef 存放,方便讓兩個 event 進一步使用

最後調整一下,完整如下:

function useEventControl(cb, delay = 2000) {
  const timeoutRef = useRef(null)
  const [isPending, setIsPending] = useState(false)
  const argsRef = useRef(null)

  const startEvent = useCallback((...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }
    argsRef.current = args
    setIsPending(true)
  }, [])

  const cancelEvent = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }
    setIsPending(false)
  }, [])

  useEffect(() => {
    if (isPending) {
      timeoutRef.current = setTimeout(() => {
        cb(...argsRef.current)
        setIsPending(false)
      }, delay)
    }

    return () => clearTimeout(timeoutRef.current)
  }, [isPending])

  return [startEvent, isPending, cancelEvent]
}

Params

Param Type Description
cb function callback,當 setTimeout 的 delay 時間到時會觸發的動作
delay number 單位ms, 定義 setTimeout 要 delay 多久,default 為 2000ms

這裡預設的delay較久,用意是給使用者時間來反應取消

Return

Return Type Description
startEvent function 來觸發event,其中傳入的參數會進一步傳入一開始的cb
isPending boolean 呈現 setTimeout 是否執行的狀態
cancelEvent function 取消evnet

這邊我自己覺得寫起來「比較卡」的地方是要能夠讓 startEvent 接受參數傳入並再丟給 cb 使用,如果有好想法歡迎再跟我分享(鞭策) (๑•́ ₃ •̀๑)

加進來

function Example() {
  const [startEvent, isPending, cancelEvent] = useEventControl(showAlert, 2000)

  return (
    <Stack>
      <Button onClick={() => startEvent("YO")} isLoading={isPending}>
        TRIGGER
      </Button>
      <Button onClick={cancelEvent}>CANCEL</Button>
    </Stack>
  )
}

我們把原本的 showAlert 傳入 useEventControl,並設定 2000ms 的 delay,再把 hook 回傳的內容放到各自的位置上。

Chakra-UI 的 <Button />,接受一個 isLoading 來呈現 UI 的狀態,我們也可以把 hook 回傳的 pending 一起傳入。

這樣一來就完成啦!

結語

相比前一篇 useDebounce,本篇實作的對 event 操控性較高,當然應用的場景也有所不同。

功能延伸上,也可以進一步加入 onDone, onCancel 來讓這個 hook 應對更多情境與狀況。


上一篇
[DAY 11] 自己的Hook自己做!useDebounce 讓使用者慢~一~點~
下一篇
[DAY 13] 自己的Hook自己做!useThroltte
系列文
React Hook 不求人,建立自己的 Hook Libary30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言