雰囲気データサイエンティストの備忘録

Atmosphere Data Scientist's Memorandum


横方向に画像をスクロールするReactコンポーネントを作る
JavaScript
React

概要

webサイトにありがちな,一定時間で横方向に画像をスクロールするコンポーネントを作ります。
スクロール

完成品

完成したコードを先に載せておきます。

基本は以下の3つです。

  1. 複数の画像を横に並べた要素を作る
  2. 画像のインデックスscrollIndexを状態変数として定義する
  3. scrollIndexが更新されたタイミングで,1.のスクロール位置を変更する

加えて,以下の3つの工夫を追加しています。
5. 画面の横幅を動的に取得する
6. 一定時間ごとにscrollIndexをインクリメントする

image-slider.tsx
import React, { useState, useRef, Component, useEffect } from 'react';


const ImageSlider = () => {
    const images: { id: number; text: string; src: string }[] = [
        { id: 0, text: '画像1', src: '/assets/blog/title/isometric.png' },
        { id: 1, text: '画像2', src: '/assets/blog/title/isometric_rear.png' },
        { id: 2, text: '画像3', src: '/assets/blog/title/isometric_under.png' },
    ];
    const [scrollIndex, setScrollIndex] = useState<number>(0);
    const [figSize, setFigSize] = useState<number>(50)
    const containerRef = useRef<HTMLDivElement>();

    /* 画面の横幅を動的に取得する
    */
    useEffect(() => {
        // divタグに埋め込んだrefから横幅を取得する
        const handleResize = () => {
            if (containerRef.current) {
                setFigSize(containerRef.current.offsetWidth);
            }
        };
        handleResize();
        // ウィンドウのサイズが変化するたびにhandleResize関数を再実行
        window.addEventListener('resize', handleResize);

        return () => {
            // アンマウント時に無効化
            window.removeEventListener('resize', handleResize);
        };
    }, []);


    /* 所定時間ごとに画像インデックスをインクリメントする
    */
    const scrollInterval: number = 7;  // 切り替え秒数を設定
    useEffect(() => {
        const interval = setInterval(() => {
            setScrollIndex((e) => (e + 1) % images.length);
        }, scrollInterval * 1000);

        return () => {
            clearInterval(interval);
        };
    }, []);


    /* 画面の横幅,または画像インデックスが変わったタイミングで,スクロール位置を調整する 
    */
    useEffect(() => {
        containerRef.current.scrollTo({
            left: scrollIndex * figSize,
            behavior: 'smooth',
        });
    }, [figSize, scrollIndex]);


    /* jsx
    */
    return (
        <div>
            <div ref={containerRef} className='flex overflow-x-hidden'>
                {images.map((image) => (
                    <img key={image.id} src={image.src} alt={image.text}></img>
                ))}
            </div>
            <div className="flex items-center justify-center">
                {images.map((image, index) => (
                    <button key={image.id} onClick={() => setScrollIndex(index)}>
                        {index == scrollIndex ? "〇" : "・"}
                    </button>
                ))}
            </div>
        </div >
    );
};


export default ImageSlider;

ポイント

1. 複数の画像を横に並べた要素を作る

    /* jsx
    */
    return (
        <div>
            <div ref={containerRef} className='flex overflow-x-hidden'>
                {images.map((image) => (
                    <img key={image.id} src={image.src} alt={image.text}></img>
                ))}
            </div>
            <div className="flex items-center justify-center">
                {images.map((image, index) => (
                    <button key={image.id} onClick={() => setScrollIndex(index)}>
                        {index == scrollIndex ? "〇" : "・"}
                    </button>
                ))}
            </div>
        </div >
    );

まずは変数imagesをmapして,画像を横並びに(flex)配置します。このままだと画像が横に延々と並んでしまうので,overflow-x-hiddenをつけて隠しましょう。
その下に,切り替えのためのボタンを配置し,各ボタンに後述のsetScrollIndex関数を割り当てます。

2. 画像のインデックスscrollIndexを状態変数として定義する

    const [scrollIndex, setScrollIndex] = useState<number>(0);    

useStateで,scrollIndexを定義します。
つまり,切り替えボタンを押すとsetScrollIndex関数が発火し,それぞれのボタンに対応したindex番号が状態変数scrollIndexに格納されます。

3. scrollIndexが更新されたタイミングで,1.のスクロール位置を変更する

    const [figSize, setFigSize] = useState<number>(500) //とりあえず500pxでスクロールする
    const containerRef = useRef<HTMLDivElement>();
    
    ....
    
    /* 画面の横幅,または画像インデックスが変わったタイミングで,スクロール位置を調整する 
    */
    useEffect(() => {
        containerRef.current.scrollTo({
            left: scrollIndex * figSize,
            behavior: 'smooth',
        });
    }, [/*figSize*/, scrollIndex]);

useRefを使ってcontainerRefという参照をjsxに埋め込みます(jsxの<div ref={containerRef}の部分と対応)。
useEffectによって,状態変数scrollIndexが変更されたときcontainerRefのスクロール位置を移動させるようにします。

ここまでで,ボタンを押して画像をスクロールするコンポーネントが作られました,
ですが,画像に対して中途半端な位置にスクロールしてしまうと思います。これは上記のfigSizeが500pxで固定されているためです。

JSXのCSSタグで画像をfigSizeと同サイズに揃えることもできるのですが,コンポーネントのサイズが変わるたびに再定義する必要が出てきます。
これを回避するために次の工夫を行います。

4. 画面の横幅を動的に取得する

    /* 画面の横幅を動的に取得する
    */
    useEffect(() => {
        // divタグに埋め込んだrefから横幅を取得する
        const handleResize = () => {
            if (containerRef.current) {
                setFigSize(containerRef.current.offsetWidth);
            }
        };
        handleResize();
        // ウィンドウのサイズが変化するたびにhandleResize関数を再実行
        window.addEventListener('resize', handleResize);

        return () => {
            // アンマウント時に無効化
            window.removeEventListener('resize', handleResize);
        };
    }, []);

先ほど参照を定義したcontainerRefから,動的に要素の横幅(containerRef.current.offsetWidth)を取得するようにします。

5. 一定時間ごとにscrollIndexをインクリメントする

    /* 所定時間ごとに画像インデックスをインクリメントする
    */
    const scrollInterval: number = 7;  // 切り替え秒数を設定
    useEffect(() => {
        const interval = setInterval(() => {
            setScrollIndex((e) => (e + 1) % images.length);
        }, scrollInterval * 1000);

        return () => {
            clearInterval(interval);
        };
    }, []);

仕上げとして,固定の秒数でscrollIndexをインクリメントします。
最後の画像まで到達すると最初の画像に戻るようにしています。

まとめ

ありがちなスクロールオブジェクトを作りました。
実は,自前実装しなくても,react-horizontal-scrolling-menu[https://www.npmjs.com/package/react-horizontal-scrolling-menu]で実現できるということに後々気が付いてしまいました…

といってもuseEffect, useStateなどの基本的なhooksの使い方が学べたので良しとします。
普段Pythonでデータの処理系ばかり書いていると,webアプリのような動的なプログラミングはなかなか難しいのですが,触ってみると面白いですね。