別名 Hero Images と呼ばれるものです。ランディングページ等で一度は使ったことがあると思います。時間差で自動スライドする一番簡単なものを紹介します。
code: github / $ yarn 1205
Component の使用感は以下の通りです。画像は background-size: cover
による指定で、表示エリア中央いっぱいに表示されます。画像比率がバラバラでも、横幅に対する高さ比率(imageRatio
)で統一調整可能になっています。
<PhotoCarousel
images={Images} // string[]
imageRatio={0.66}
transitionInterval={4000}
transitionDuration={400}
onChangeCurrent={setCurrent}
/>
Component 概要図
画像は数枚固定を想定しているので、今回はいきなり全てマウントします。画像 Component を設置する Container 幅は「画像数 * 画面幅」を指定。各々の画像 Component 座標は index分ずらして設置。current を切り替えることで Container 座標を移動させます。animation 動力は CSS animation です。
State Bubbling
PhotoCarousel に保持している current は、Indicate
Component の関心ごとでもあります。(↓)
current の変更を外部に伝播する手段として、変更に応じて callback 関数をトリガーする effect を追加します。PhotoCarousel Component の親Component は、別途 current を保持し、自身の setCurrent 関数を callback props にバインドします。こうする事で、子Component の State を吸い上げることができます。
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 概要です。
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 に表示エリア比率の元になる、縦横を保持します。
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 対象としています。
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 があった場合、バインドされた関数を叩きます。
useEffect(
() => {
if (options.onChangeCurrent === null) return
options.onChangeCurrent(state.current)
},
[state.current]
)