Photo Carousel

別名 Hero Images と呼ばれるものです。ランディングページ等で一度は使ったことがあると思います。時間差で自動スライドする一番簡単なものを紹介します。
code: [github](https://github.com/takefumi-yoshii/react-hooks-ogiri/tree/master/apps/photo-carousel) / `$ yarn 1205`

|![1205-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/bc4792d2-fb89-b2c9-3746-db2e74cc516b.jpeg)|![1205.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/384b0f5c-2115-6f28-3b6a-6e31135a2abb.jpeg)|
|---|---|

Component の使用感は以下の通りです。画像は `background-size: cover` による指定で、表示エリア中央いっぱいに表示されます。画像比率がバラバラでも、横幅に対する高さ比率（`imageRatio`）で統一調整可能になっています。

```components/hero.tsx
<PhotoCarousel
  images={Images} // string[]
  imageRatio={0.66}
  transitionInterval={4000}
  transitionDuration={400}
  onChangeCurrent={setCurrent}
/>
```
# Component 概要図

画像は数枚固定を想定しているので、今回はいきなり全てマウントします。画像 Component を設置する Container 幅は「画像数 * 画面幅」を指定。各々の画像 Component 座標は index分ずらして設置。current を切り替えることで Container 座標を移動させます。animation 動力は CSS animation です。

![1205.png](https://qiita-image-store.s3.amazonaws.com/0/59071/4b7f2214-b07e-ac92-0e56-35ca0c00788d.png)

# State Bubbling

PhotoCarousel に保持している current は、`Indicate` Component の関心ごとでもあります。(↓)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59071/10057bbf-4352-bf75-255e-9128e4925b09.png)

current の変更を外部に伝播する手段として、変更に応じて callback 関数をトリガーする effect を追加します。PhotoCarousel Component の親Component は、**別途 current を保持**し、自身の setCurrent 関数を callback props にバインドします。こうする事で、子Component の State を吸い上げることができます。


```components/hero.tsx
const View = (props: Props) => {
  const [current, setCurrent] = useState(0) // here
  return (
    <div className={props.className}>
      <PhotoCarousel
        images={Images}
        imageRatio={0.66}
        transitionInterval={4000}
        transitionDuration={400}
        onChangeCurrent={setCurrent} // here
      />
      <div className="indicate">
        <Indicate
          current={current} // here
          color="light"
          count={Images.length}
        />
      </div>
    </div>
  )
}
```

State Bubbling は手軽に利用出来る反面、多くのComponent の関心ごととして高階層に伝播するのには向きません。

- 同じ値を表現したいものが重複していること
- 階層毎に Hooks API と callback関数が必要なこと
- 単方向データフローの逆行とも捉えられること

この程度のコンテキストに閉じるには一つの手段として使えますが、早期に ContextAPI か reducer に乗せるのが適切でしょう。

# usePhotoCarousel

今回定義している Custom Hooks 概要です。

```components/photoCarousel/usePhotoCarousel.tsx
const usePhotoCarousel = (props: Props) => {
  const [state, update] = useState<State>(...)
  const options = useMemo(...)
  const nodeStyle = useMemo(...)
  const containerStyle = useMemo(...)
  useEffect(...) // 画面リサイズに対応する effect
  useEffect(...) // 所定時間で切り替える effect
  useEffect(...) // current 変更の callback を叩く effect
  return {
    current: state.current,
    renderItems,
    nodeStyle,
    containerStyle
  }
}
```


# 画面リサイズに対応する effect

State に表示エリア比率の元になる、縦横を保持します。

```components/photoCarousel/usePhotoCarousel.tsx
useEffect(() => {
  const handleResize = () => {
    if (props.ref.current === null) return
    const {
      width,
      height
    } = props.ref.current.getBoundingClientRect()
    update(_state => ({ ..._state, width, height }))
  }
  handleResize()
  window.addEventListener('resize', handleResize)
  return () =>
    window.removeEventListener('resize', handleResize)
}, [])
```

# 所定時間で切り替える effect

途中でインターバルが変更することもあまりないですが、memoize 対象としています。

```components/photoCarousel/usePhotoCarousel.tsx
useEffect(
  () => {
    const id = setInterval(() => {
      update(_state => {
        const current =
          _state.current === options.imagesCount - 1
            ? 0
            : _state.current + 1
        return { ..._state, current }
      })
    }, options.transitionInterval)
    return () => clearInterval(id)
  },
  [options.transitionInterval]
)
```

# current 変更の callback を叩く effect

Optional Injection があった場合、バインドされた関数を叩きます。

```components/photoCarousel/usePhotoCarousel.tsx
useEffect(
  () => {
    if (options.onChangeCurrent === null) return
    options.onChangeCurrent(state.current)
  },
  [state.current]
)
```