自製日期選擇器-2 (custom hook)

延續 上一集
確定元件的輸入輸出介面後
(回顧四個基本屬性: isVisible, displayDate, mode, onConfirm)
現在要開始實作功能啦

我們可以把實作步驟切分成幾個部分:

  1. 排版: 如何動態顯示每個月的日期數以及星期
  2. 單日日期操作: 每次點選都會更新 data.date
  3. 多日日期操作: 有時點選是設定開始日date.startDate,有時是結束日endDate,需設計操控流程
  4. 效能問題: 目前為止每次當元件內有一堆按鈕的時候都會有不可忽略的卡頓(因為每次點擊都會讓每個按鈕重新渲染),希望我能研究出如何減少這個卡頓時間

今天這章節就來看看如何做日期的動態顯示吧!

動態顯示日期

首先我們來想一想
一般的日曆app都怎麼排版?
每個月第一天的星期都不一樣
並且前後分別會補上個月最後幾天和下個月前幾天
直到補滿整行,像這樣(網路找的圖):

要達到這個目標,必須先有三個資訊:

  1. 本月有幾天? days
  2. 本月第一天是星期幾? firstDay
  3. 上個月有幾天? prevMonthDays

所以我打算做一個useDaysOfMonthcustom hook 來提供這些訊息

  • Custom Hook 是一組包裝起來的邏輯,有自己的 life cycle,你可以在其中使用useStateuseEffect之類的 React hook。如果你還記得,hook 不是有很多使用上的限制嗎? 例如只能存在於元件內的第一層而不能被包在元件內的函數內,這項限制讓你沒辦法把重複用的邏輯包起來。而 custom hook 就是為此而生。

Custom Hook: useDaysOfMonth

custom hook 有個條件,就是一定要是use開頭,如此一來 React 運行時才能判斷元件內的 hook 是不是都在最上層,所以別忘了這步。
除此要求之外,一個 custom hook 看起來就像是不做渲染的 React 元件,或是可以放 hook 的一般函數。

那就開始吧,首先我們建立一個檔案叫 useDaysOfMonth.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// in useDaysOfMonth.jsx
import { useState } from 'react'

const useDaysOfMonth =(inputTime)=>{ //輸入一個時間,這會是來自元件的 displayDate 屬性
const [days, setDays] = useState(0);
const [firstDay, setFirstDay] = useState(0)
const [prevMonthDays, setPrevMonthDays] = useState(0);

// 這裡要做點事

return {days, firstDay, prevMonthDays} //輸出三個我們要的資訊,皆為 number type
}

export default useDaysOfMonth

接下來我們需要拆解輸入進來的日期物件,得到年份,月份

1
2
const year = inputTime.getFullYear()
const month = inputTime.getMonth()

然後我們就要依照年月來獲得 days, firstDay, prevMonthDays 三個資訊
*注意不要用日期物件的setDate來做這些事,因為Date()的 method 都會 mutate(會改變物件自身的值,而不會另外建立一個物件)

1
2
3
4
5
6
7
8
9
10
11
12
setDays( new Date(year,month+1,0).getDate() )
// 在 Date constructor 中,如果日期填0,則會視為上個月的最後一天
// new Date(2021,2,1) => 三月一號
// new Date(2021,2,0) => 二月二十八號
// new Date(2021,2,-1) => 二月二十七號
// 所以若要獲得本月最後一天的日期,只要把月份+1,然後日期設為0即可

setFirstDay(new Date(year,month,1).getDay())
// 很直觀,取本月第一天的星期

setPrevMonthDays(new Date(year, month, 0).getDate())
// 同 setDays() 不過月份不用+1

過程很簡單,都是基本的Date物件取值
再來我們希望在任何使用這個 hook 的元件中,每當 input 值改變時,輸出值也要跟著更新
這不就是useEffect的功能嗎!
所以我們將以上運算放進useEffect中,並將inputTime設為 dependency

1
2
3
4
5
6
7
useEffect(()=>{
let year = inputTime.getFullYear()
let month = inputTime.getMonth()
setDays(new Date(year, month + 1, 0).getDate())
setFirstDay(new Date(year, month, 1).getDay())
setPrevMonthDays(new Date(year, month, 0).getDate())
},[inputTime])

完成! 完整的code如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react'

const useDaysOfMonth = (inputTime) => {
const [days, setDays] = useState(0);
const [firstDay, setFirstDay] = useState(0)
const [prevMonthDays, setPrevMonthDays] = useState(0);

useEffect(() => {
let year = inputTime.getFullYear()
let month = inputTime.getMonth()
setDays(new Date(year, month + 1, 0).getDate())
setFirstDay(new Date(year, month, 1).getDay())
setPrevMonthDays(new Date(year, month, 0).getDate())
}, [inputTime])
return { days, firstDay, prevMonthDays }
}

export default useDaysOfMonth

一個小細節,我把輸出的參數用大括號(object literal)包起來,這樣在存取參數時就不需考慮順序
什麼意思呢? 看以下的使用方式就知道了

1
const {firstDay, prevMonthDays, days} = useDaysOfMonth(new Date())

可以看到我取得三個參數的順序不同,但仍然會work
缺點是這樣就一定要把參數名稱打對了,想要更改參數名就要打{firstDay: xxx}
反之,如果你回傳的是用中括號(array literal)包起來[firstDay, prevMonthDays, days]
取得參數時的順序就很重要了,不過就可以直接改名