React で遅延読み込み機構を作ってみました。
デモ
github.com
コード
useShown
と useUpdateHeight
という2つのフックを使って実現します。useShown はスクロール状況から「読み込みを開始すべきか」を判定します。useUpdateHeight はコンポーネントの高さをストアへ伝えます。
import {
FC,
useEffect,
useRef,
useState,
createContext,
Children,
useCallback,
} from 'react';
const HeightContext = createContext<
[number[], (index: number, height: number) => void]
>([[], () => null]);
const IndexContext = createContext(0);
const LazyLoadWrapper: FC = ({ children }) => {
const [height, setHeight] = useState<number[]>([]);
const updateHeight = useCallback((index: number, height: number) => {
setHeight((arr) => {
const newValue = arr.concat();
newValue[index] = height;
return newValue;
});
}, []);
return (
HeightContext.Provider value={[height, updateHeight]}
{Children.map(children, (child, index) => (
IndexContext.Provider value={index} key={index}
{child}
IndexContext.Provider
))}
HeightContext.Provider
);
};
const Child = () => {
const divRef = useRef<HTMLDivElement>(null);
const shown = useShown();
const updateHeight = useUpdateHeight();
const height = useRef((Math.floor(Math.random() * 9) + 1) * 100).current;
useEffect(() => {
if (divRef.current && shown) {
updateHeight();
}
}, [shown, updateHeight]);
if (!shown) {
return null;
}
return div style={{ width: 100, height }} ref={divRef}div;
};
export const App= () => {
return (
LazyLoadWrapper
Child
Child
Child
Child
Child
LazyLoadWrapper
);
};
各コンポーネントで高さを伝達し、LazyLoadWrapper で囲むだけで遅延読み込みが実現できます。
useShown と useUpdateHeight の実装
結構シンプルです。useShown は「まだマウントしてない要素のうち一番上にあって、かつスクロール量が閾値を超えているか」を判定しているだけです。閾値は適当に「画面の高さ * 3」だけの余裕が取れるようにしています。
useUpdateHeight はさらにシンプルで、HeightContext の配列を更新するだけです。
import { useEffect, useState, useCallback, useContext } from 'react';
const useLazyload = () => {
const index = useContext(IndexContext);
const [height] = useContext(HeightContext);
const [shown, setShown] = useState(false);
useEffect(() => {
if (shown) {
return;
}
const nextIndexToMount = (() => {
const indexOfFirstUndefined = height.findIndex(
(item) => typeof item === 'undefined'
);
if (indexOfFirstUndefined !== -1) {
return indexOfFirstUndefined;
}
return height.length;
})();
const totalHeight = height
.slice(0, nextIndexToMount)
.reduce((a, b) => a + b, 0);
const listener = () => {
if (
nextIndexToMount === index &&
totalHeight < 3 * window.innerHeight + window.scrollY
) {
setShown(true);
}
};
window.addEventListener('scroll', listener);
return () => {
window.removeEventListener('scroll', listener);
};
}, [height, index, shown]);
return shown;
};
const useUpdateHeight = () => {
const index = useContext(IndexContext);
const [, setHeight] = useContext(HeightContext);
return useCallback(
(height: number) => setHeight(index, height),
[index, setHeight]
);
};