LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

Organization

Pull Fetcher

画面を上いっぱいまでスクロールし、引っ張る事で最新のデータを fetch する、アプリでお馴染みの機能です。
code: github / $ yarn 1214

1214.jpg 1214-1.jpg

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>
  )
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1