preactで無限スクロール実装する

# 概要
reactで無限スクロールするなら、ライブラリがたくさんある
https://github.com/danbovey/react-infinite-scroller

ただ、今回preactで開発していて、どうせならreactライブラリ使わずに簡単にできないかを考えた。

# 実装

下記の実装を作成。
```typescript
import { h } from "preact";
import style from "./style.module.css";

/**
 *
 * @param loadMore scroll時に実施するmethod
 * @param children スクロール対象の要素
 * @param height スクロール範囲の高さ
 * @param margin 下から何pxの部分でロードをかけるかの値。default:20
 * @param hasMore 追加で読み込むかの判別flag。fefault:true
 * @param pageStart 追加ロードで読み込むpage。default:0
 * @returns
 */
const InfiniteScroll = ({
  loadMore,
  children,
  height,
  margin = 20,
  hasMore = true,
  pageStart = 0,
}: {
  loadMore: (page: number) => void;
  children: h.JSX.Element;
  height: number;
  margin?: number;
  hasMore?: boolean;
  pageStart?: number;
}) => {
  const onScrollHandler: h.JSX.UIEventHandler<HTMLDivElement> = (e) => {
    if (!hasMore) return;
    if (
      e.currentTarget.scrollHeight -
        e.currentTarget.offsetHeight -
        e.currentTarget.scrollTop <
      margin
    ) {
      loadMore(pageStart + 1);
    }
  };

  return (
    <div
      class={style["scroll-component"]}
      style={{ height: height }}
      onScroll={onScrollHandler}
    >
      {children}
    </div>
  );
};

export default InfiniteScroll;

```
cssは下記の通り。

```css
.scroll-component {
  overflow-y: scroll;
}
```

`loadMore`にはpageを受け取って追加読み込みする関数を渡す
`children`は`InfiniteScroll`の子要素としてセットし、無限読み込みしたいlistなどを配置する
`height`はスクロールする要素の高さ
`margin`は下から何pxの部分で追加読み込みするか決める値
`hasMore`は追加読み込みするかどうかの条件
`pageStart`は読み込むページの初期値。追加読み込みするときは、`pageStart+1`のページを読み込む。

# サンプル
下までスクロールされたら追加読み込みしてlistを増やすサンプルを示す。
サンプルコードなので、handlerやテストデータの作り方は雑です。

```typescript
import h from "preact";
import { useState } from "preact/hooks";
import InfiniteScroll from "./infinite-scroll";

export const Sample = () => {
  const initTestData = [
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
    { key: "TestKey", value: "TestValue" },
  ];
  const [list, setList] =
    useState<{ key: string; value: string }[]>(initTestData);
  const [hasMore, setHasMore] = useState<boolean>(true);
  const [currentPage, setCurrentPage] = useState<number>(0);
  const loadMore = (page: number) => {
    const testData = initTestData.map((data) => {
      return { key: data.key + page, value: data.value + page };
    });
    setList([...list, ...testData]);
    setCurrentPage(page);
  };
  const reset = () => {
    setCurrentPage(0);
    setList(initTestData);
  };
  return (
    <div>
      <button onClick={reset}>リセット</button>
      <button
        onClick={() => {
          setHasMore(false);
        }}
      >
        読み込み停止
      </button>
      <InfiniteScroll
        loadMore={loadMore}
        hasMore={hasMore}
        margin={5}
        pageStart={currentPage}
        height={300}
      >
        <div style={{ border: "solid 1px" }}>
          <ul>
            {list.map((l) => (
              <li>
                {l.key}:{l.value}
              </li>
            ))}
          </ul>
        </div>
      </InfiniteScroll>
    </div>
  );
};


```

- 最初は初期値だけlistに入っている
![スクリーンショット 2022-03-24 13.04.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188058/d970737b-8e23-619e-daff-5cc2973f0c40.png)

- 下までスクロールすると追加でlistに追加されていく

![スクリーンショット 2022-03-24 13.05.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188058/dbebd71e-e8ca-e81f-7577-7827b8b8d273.png)

- 読み込み停止(hasMore=false)すると下まで行っても追加読み込みしない

![スクリーンショット 2022-03-24 13.06.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188058/43b85ac8-a54a-1a46-e95b-04d9266ee30a.png)


# まとめ
やってみたらすごい少ない行数で無限スクロールが実現できました。

また、ほぼreactと同じ使い方できるので、reactのライブラリを使わず自作でカスタムしたい人にも参考になるかもです。

ライブラリに頼りすぎるとコーディングスキル落ちそうだから簡単な実装程度なら自力で描いてかないとダメだね。