👌

react-useを使ってカルーセルを作ってみた

2023/03/17に公開
2

はじめに

こんにちは!
スペースマーケットでフロントエンドエンジニアをしている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を返すものになります。

https://github.com/streamich/react-use/blob/master/docs/useIntersection.md

IntersectionObserverとは

特定の領域を監視し、監視対象の要素がその領域に入ったかを常に監視し検知してくれるものです。
監視対象が特定の領域と交差するとcallback関数を実行してくれます。
このcallback関数の引数にIntersectionObserverEntryという交差した時の情報を格納したオブジェクトが渡ってくるようになります。

https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API

では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を使って実装なり記事にまとめてみたいなと思いました。

最後に

スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
とりあえずどんなことをしているのか聞いてみたいという方も大歓迎です!
ご興味ありましたら是非ご覧ください!
https://www.wantedly.com/projects/1113570
https://www.wantedly.com/projects/1113544
https://www.wantedly.com/projects/1061116
https://spacemarket.co.jp/recruit/engineer/

スペースマーケット Engineer Blog

Discussion

kiyomasa.satokiyomasa.sato

AndroidスマホのChromeでみましたが

  • 1スワイプで2ページ飛ぶ
  • インジケーターが動かない
  • タップしないと矢印が出ない
  • 矢印が2ページ以降動かない

など軽く触っただけでも不具合だらけだったと報告します。

k___0122k___0122

コメントありがとうございます。
thresholdの値を調整したので、Android Chromeでもスクロールできるようになったかと思いますmm
端末によって交差したという判定が微妙に異なるようでした。。

タップしないと矢印が出ない

矢印はPCでホバーした場合のみ表示するようにしていたのですが、常時表示するように変更しております。

1スワイプで2ページ飛ぶ

こちらは考慮漏れでした🙇‍♂️
scroll-snap-stop: always;を追加したので、スワイプした場合各スナップポイントで停止するようになっているかと思います。
ご指摘ありがとうございました。