Pull Fetcher

画面を上いっぱいまでスクロールし、引っ張る事で最新のデータを fetch する、アプリでお馴染みの機能です。
code: [github](https://github.com/takefumi-yoshii/react-hooks-ogiri/tree/master/apps/pull-fetcher) / `$ yarn 1214`

|![1214.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/2501393a-63db-c61e-402a-1e4d5bb8f758.jpeg)|![1214-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/17958c00-17f8-0743-a93b-6ad1f15fe576.jpeg)|
|---|---|

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