LoginSignup
1
1

Reactで無限スクロールを実装する。(Handson,ライブラリなし)

Last updated at Posted at 2023-10-28

はじめに

表題通り、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
index.tsx
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>
);
App.tsx
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;
}

src/hooks/useInfinityScroll.tsx
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;
};

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1