画面を上いっぱいまでスクロールし、引っ張る事で最新のデータを fetch する、アプリでお馴染みの機能です。
code: github / $ yarn 1214
usePullFetcher
今回の Custom Hooks 内訳です。
components/usePullFetcher.ts
function usePullFetcher() {
const [state, update] = useState<State>(...)
const handleTouchDown = useCallback(...)
const handleTouchUp = useCallback(...)
const handleTouchMove = useCallback(...)
useEffect(...)
useEffect(...)
return {
state,
handleTouchDown,
handleTouchUp,
handleTouchMove
}
}
触れた瞬間の処理
タッチフラグをオンにします。
components/usePullFetcher.ts
const handleTouchDown = useCallback(
(event: TouchEvent<HTMLElement>) => {
event.persist()
update(_state => ({ ..._state, isTouching: true }))
},
[]
)
指を動かしている時の処理
window.scrollY が 0px でない場合、early return します。「勢いよく引っ張った」判定をするため、前回同イベント発生時の座標を保持します。閾値が大きいほど「勢い」が必要になります。
components/usePullFetcher.ts
const handleTouchMove = useCallback(
(event: TouchEvent<HTMLElement>) => {
event.persist()
update(_state => {
if (!_state.isTop) return _state
const y = event.touches[0].clientY - _state.startY
const offsetY = y > 0 ? _state.threshold : 0
const startY = event.touches[0].clientY
return {
..._state,
isTouching: true,
startY,
offsetY
}
})
},
[]
)
離した瞬間の処理
軽く触れて離された場合、処理が走らない様にブロックします。
components/usePullFetcher.ts
const handleTouchUp = useCallback(
(event: TouchEvent<HTMLElement>) => {
event.persist()
update(_state => {
const offsetY =
_state.offsetY > 0 ? _state.threshold : 0
const fetched = offsetY === 0
return {
..._state,
fetched,
isTouching: false,
offsetY
}
})
},
[]
)
useEffect
今回はサーバーを立てていないので簡易的な処理を施していますが、async function を useEffect で即時実行することで await する事が出来ます。
components/usePullFetcher.ts
useEffect(() => {
const handleScroll = () => {
update(_state => {
const isTop = window.scrollY === 0
return { ..._state, isTop }
})
}
handleScroll()
window.addEventListener('scroll', handleScroll)
return () =>
window.removeEventListener('scroll', handleScroll)
}, [])
useEffect(
() => {
;(async () => {
if (state.fetched) return
try {
await wait(400)
const newItems = getMailItems(1)
const items = [
...state.items,
...newItems.map(item => ({
...item,
date: new Date(item.date)
}))
].sort(
(a, b) => b.date.getTime() - a.date.getTime()
)
update(_state => ({
..._state,
fetched: true,
offsetY: 0,
items
}))
} catch (error) {
update(_state => ({
..._state,
fetched: true,
offsetY: 0,
error
}))
}
})()
},
[state.fetched]
)
render props
Custom Hooks を利用するコンポーネントを用意、子コンポーネントに状態変化を伝搬させる時のみ render props します。
components/container.tsx
const View = (props: Props) => {
const {
state,
handleTouchDown,
handleTouchUp,
handleTouchMove
} = usePullFetcher()
return (
<div
className={props.className}
onTouchStart={handleTouchDown}
onTouchEnd={handleTouchUp}
onTouchCancel={handleTouchUp}
onTouchMove={handleTouchMove}
>
{useMemo(() => props.render(state), [
state.offsetY,
state.fetched
])}
</div>
)
}