react-useを使ってカルーセルを作ってみた
はじめに
こんにちは!
スペースマーケットでフロントエンドエンジニアをしているk___0122です。
今回はreact-useのuseIntersectionを使って以下のようなカルーセルを作ったので紹介したいなと思います!
今回作るカルーセルは以下の仕様で実装します。
- スクロールができること
- インジケーターが現在表示されてる画像に合わせて活性化すること
- 矢印をクリックしたら画像が切り替わること
実装してみる
スクロールができることは以下のCSSのみでできます。
overflow-x: scroll;
scroll-snap-type: x mandatory;
overflow-x: scrollではみ出した要素をスクロールできるようにします。
またscroll-snap-type: x mandatoryをつけることで中途半端にスクロールをしていても、スクロール位置から前の写真または次の写真までよしなにスクロールしてくれます。
インジケータの実装はスクロールした時に現在表示されてる写真のindexを取得し、写真のindexと比較すれば実装できそうです。
ただ画像がスクロールしたという判定をどうしたらいいのか分からず悩んでいました。
今回はReactを使って実装したかったので色々と調べてみると、react-useにuseIntersectionというhooksがありました。
useIntersectionとは
useIntersectionはIntersectionObserverAPIを使用し、IntersectionObserverEntryを返すものになります。
IntersectionObserverとは
特定の領域を監視し、監視対象の要素がその領域に入ったかを常に監視し検知してくれるものです。
監視対象が特定の領域と交差するとcallback関数を実行してくれます。
このcallback関数の引数にIntersectionObserverEntryという交差した時の情報を格納したオブジェクトが渡ってくるようになります。
ではuseIntersectionを使ってインジケーターの実装をしてみます。
const listItemRef = useRef<HTMLLIElement>(null)
const intersection = useIntersection(listItemRef, {
threshold: 1,
})
useEffect(() => {
if (intersection?.intersectionRatio === 1) {
setCurrentThumbnailIndex(index)
}
}, [
intersection?.intersectionRatio,
setCurrentThumbnailIndex,
])
まずuseRefを用意し、useIntersectionの第一引数にrefオブジェクトを、第二引数にオプションオブジェクトを渡します。
thresholdを1と指定することで、監視対象の要素が100%表示されたらuseIntersectionの返す値が切り替わるようになります。
スクロールしたかどうかの判定は、IntersectionObserverEntryのintersectionRatioで判定します。
intersectionRatioは監視対象の要素が領域と交差している割合を数値で返してくれます。
今表示されてる写真のindexを取得するには、intersectionRationが1の時にsetStateすればよさそうですね。
これでスクロールする度に今表示されてる写真のindexをstateで管理できるようになりました。
インジケーターの実装は以下のようになります。
{ landscapeThumbnails.map((_, index) => (
<span
key={`indicator-${index}`}
className={`
${currentThumbnailIndex === index ? 'bg-white' : 'bg-gray-500'}
w-[8px]
h-[8px]
rounded-full
border-solid
`}
/>
))}
最後に矢印をクリックした時の実装をしていきます。
まず以下のようなdata属性を持たせたliタグを用意します。
<li
className="list-none snap-start"
ref={listItemRef}
data-image-id={`image-id:${index}`}>
<img
className="min-w-[330px] h-[300px]"
src={Thumbnail.src} alt={Thumbnail.alt}
/>
</li>
矢印をクリックしたらliにあるdata属性を取得しscrollToをすることで画像の切り替えができるようになります。
const scrollTo = (index: number): void => {
const target = document.querySelector<HTMLLIElement>(
`[data-image-id="image-id:${index}"]`,
)
if (!target) return
target.parentElement?.scrollTo?.(target.offsetLeft, 0)
}
const scrollToPrevImage = (): void => scrollTo(currentThumbnailIndex - 1)
const scrollToNextImage = (): void => scrollTo(currentThumbnailIndex + 1)
これで完成です!
感想
useIntersectionを使うことで手軽にスクロール系の実装ができるようになりました!
またIntersection Observerについてもっと深掘りしたいなと思ったので、Intersection Observerを使って実装なり記事にまとめてみたいなと思いました。
最後に
スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
とりあえずどんなことをしているのか聞いてみたいという方も大歓迎です!
ご興味ありましたら是非ご覧ください!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion
AndroidスマホのChromeでみましたが
など軽く触っただけでも不具合だらけだったと報告します。
コメントありがとうございます。
thresholdの値を調整したので、Android Chromeでもスクロールできるようになったかと思いますmm
端末によって交差したという判定が微妙に異なるようでした。。
矢印はPCでホバーした場合のみ表示するようにしていたのですが、常時表示するように変更しております。
こちらは考慮漏れでした🙇♂️
scroll-snap-stop: always;
を追加したので、スワイプした場合各スナップポイントで停止するようになっているかと思います。ご指摘ありがとうございました。