使用React实现无限滚动。(无需任何手册或库)
首先
如标题所述,我们将使用React来实现无限滚动。我们没有使用任何库,而是利用IntersectionObserver来创建。
项目成果
源代码
~/develop/HITOTSU/react_infinity_scroll$ tree -I node_modules
.
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.tsx
│ ├── hooks
│ │ └── useInfinityScroll.tsx
│ ├── index.tsx
│ └── logo.svg
├── tsconfig.json
└── yarn.lock
4 directories, 14 files
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
import React, { useRef } from 'react';
import { useInfinityScroll } from './hooks/useInfinityScroll'
function App() {
const containerRef = useRef(null);
const fetchData = async (page: number) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}`);
const data = await response.json();
return data;
};
const data: Post[] = useInfinityScroll(containerRef, fetchData);
return (
<div>
<div style={{height: '2000px'}}>無限スクロール開始</div>
<div ref={containerRef}>
{data.map((item: Post) => (
<div key={item.id}>
<p>
{item.id}:{item.title}
</p>
</div>
))}
</div>
</div>
);
}
export default App;
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
import {RefObject, useCallback, useEffect, useState} from 'react';
const options ={
root: null, // ルート要素 (viewport) を使用
rootMargin: '0px',
threshold: 0, // 要素が少しでもビューポートに表示された瞬間からコールバックが呼び出される
}
export const useInfinityScroll = <T,>(ref: RefObject<HTMLElement | null>, fetch: (page: number) => Promise<T[]>) => {
const [data, setData] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false); // 読み込み中のフラグ
const [hasMoreData, setHasMoreData] = useState(true); // 追加データがあるかどうかを追跡
const scrollObserver = useCallback(
() =>
new IntersectionObserver((entries) => {
console.log('entries', entries);
entries.forEach((entry) => {
if (entry.isIntersecting && !isLoading && hasMoreData) {
setIsLoading(true); // 読み込み中フラグを設定
fetch(page).then((_data) => {
console.log('fetch call page', page);
if (_data.length > 0) {
setPage(page + 1);
setData((oldValue) => [...oldValue, ..._data]);
} else {
// 追加データがない場合
setHasMoreData(false);
}
setIsLoading(false);
});
}
});
},options),
[page, fetch, isLoading, hasMoreData]
);
useEffect(() => {
const target = ref.current;
if (target) {
const observer = scrollObserver();
observer.observe(target);
return () => {
observer.unobserve(target);
};
}
}, [scrollObserver, ref]);
return data;
};