
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]);
意外と使用することがありますが、忘れてしまうことが多いので、ここに残しておきたいと思います。
それでは。