見出し画像

Reactでoverflow:scrollを制御する

Reactでoverflow-x: scrollのDOM制御を行う際のポイントをまとめておきます。ReactにおいてDOMを制御する場合、useRefを使用します。
まず、以下は全体のコードです。

// useScroll.ts

import { RefObject, useRef } from 'react';

// -----------------------------
// interface
// -----------------------------
export type IuseScrollProps = {
    outerContentRef: RefObject<HTMLDivElement>;
    innerContentRef: RefObject<HTMLUListElement>;
    setScrollPosition: (position: number) => void;
    getScrollWidth: () => number;
};

// -----------------------------
// hooks
// -----------------------------
export const useScroll = (): IuseScrollProps => {
    const innerContentRef = useRef<HTMLUListElement>(null);
    const outerContentRef = useRef<HTMLDivElement>(null);
    
    /**
     * 画面初期化時にスライドの位置を変更する
     */
    const setScrollPosition = (position: number): void => {
      const outerRef = outerContentRef.current;
      if (outerRef) {
        outerRef.scrollLeft = position;
      }
    };
    
    /**
     * 現在の要素の横幅を取得してセンターのスクロール位置を取得する
     */
    const getScrollWidth = (): number => {
      const innerRef = innerContentRef.current;
      const outerRef = outerContentRef.current;
    
      if (innerRef && outerRef) {
        const innerWidth = innerRef.offsetWidth;
        const outerWidth = outerRef.offsetWidth;
        return (innerWidth - outerWidth) / 2;
      }
      return 0;
    };
    
    return {
        innerContentRef,
        outerContentRef,
        setScrollPosition,
        getScrollWidth
    };
};

// Scroll.tsx    
import React, { ReactElement, useEffect } from 'react';
import { useScroll } from 'hooks/useScroll';
    
// -----------------------------
// Component
// -----------------------------
export const Scroll = (): ReactElement => {
    const classes = useStyles();
    const scroll = useScroll();
    
    // -----------------------------
    // LifeCycle
    // -----------------------------
    useEffect(() => {
      scroll.setScrollPosition(scroll.getScrollWidth());
    }, [scroll]);
    return (
      <>
        <div>
          <p>
            <span>WorkPointで</span>
            <span>ポイント稼いでます!</span>
          </p>
        </div>
        <div>
       
          // refのついた2つのdivとulにはstyleでwidth指定が無いとDOMで横幅を取得できないので必ず指定しておきます。
          <div ref={scroll.outerContentRef} className={classes.cardWrapper}>
            <ul ref={scroll.innerContentRef} className={classes.cardList}>
              <li>
                // Listの中身が入る
              </li>
            </ul>
          </div>
        </div>
      </>
    );
};

// -----------------------------
// Style
// -----------------------------
// ここではMaterial UIの記述ですが設定されているプロパティを参考にしてください
cardWrapper: {
    overflow: 'scroll',
    margin: '40px auto 0',
    width: '100%',
    maxWidth: '100%',
},

cardList: {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    flexWrap: 'nowrap',
    margin: '0 auto',
    maxWidth: '720px',
    width: '720px',
 
    '& > li': {
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        flexDirection: 'column',
        scrollSnapAlign: 'center',
        margin: '0 8px',
        height: '240px',
    },
},

useRefが宣言されているHoooksの部分を見ていきましょう。

const innerContentRef = useRef<HTMLUListElement>(null);
const outerContentRef = useRef<HTMLDivElement>(null);

こちらでは、DivElementとULitsElementを型として持つuseRefを初期化しています。
これは、JSX側のスクロールをラップしている外側のDOMと、実際にスクロールされるリストの内側のDOMを取得する必要があるためです。
Hooks内のメソッドで行われることは非常に簡単です。
コード内のコメントにも記載されているように、以下の処理を行うメソッドを作成しています。

● setScrollPosition
画面初期化時にスライドの位置を変更します。

● getScrollWidth
現在の要素の横幅を取得し、スクロールXのセンター位置を計算します。
これにより、(innerWidth - outerWidth) / 2の値を使用してスクロールのX座標をセンターに位置させることが可能です。
注意点として、innerWidthとouterWidthのエレメントのwidthを指定しないと正しく計算されず、結果がおかしくなる可能性があるため、必ず指定するようにしましょう。

残りの部分では、useEffect内でメソッドを呼び出し、setScrollPosition()を使用して位置をセットするだけで、マウント時に要素のscrollXがセンターになります。

useEffect(() => {
  scroll.setScrollPosition(scroll.getScrollWidth());
}, [scroll]);

意外と使用することがありますが、忘れてしまうことが多いので、ここに残しておきたいと思います。

それでは。

いいなと思ったら応援しよう!