無限スクロールの反応


概要


この投稿ではHTTPリクエストを行いますOpenLibrary そして、クエリにマッチするブックタイトルの名前を取得します.それから、それらの結果はページ化されて、表示されます.ドキュメントの次のバッチは、最終ドキュメントにスクロールしたユーザー、すなわち、それがスクリーンに描かれるときに、フェッチされる.

工具


私たちは、私たちのHTTPリクエストを作るためのロジックを形成するUSENT、UseEffect、Useref、usecallbackとカスタムフックなどの反応フックを使用します.また、私たちは、論理を簡素化するのを助けるAxiosを使用するでしょう.

ステップ1 -初期化


CodesandBoxに行き、新しい反応プロジェクトを初期化しましょう.react.new

十分簡単.

ステップ2 -要素


今のところ、我々は入力フィールドを表示することができます、いくつかのdivは、本のタイトルと2つのH 3タグは、ローディングメッセージとエラーメッセージが表示されますを表します.
import React from "react";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h1>React infinite scroll</h1>
      <input type="text" />
      <div>Book Title</div>
      <div>Book Title</div>
      <div>Book Title</div>
      <div>
        <h3>Loading...</h3>
      </div>
      <div>
        <h3>There seems to be an error</h3>
      </div>
    </div>
  );
}
これにより次のレイアウトが得られます.

今のところ、我々は機能に焦点を当て、後のポストでスタイリングを追加します.

ステップ3 - HTTPリクエストを作成する


ファイルを作成しましょうuseGetData.js 内部src . Axiosをインストールしましょうnpm i axios . さて、AxiosからAxEstateとUseeffectをインポートしてみましょう.
import { useState, useEffect } from "react";
import axios from "axios";
それは私たちが働くために我々のカスタムフックのために輸入する必要があるすべてです.
では、関数を定義しましょうquery パラメータとpageNumber を指定し、loading and error ステートbooks すべての本とAを含む配列hasMore 私たちが結果の終わりに達したとき、API呼び出しをやめるとき、決定する変数.
export default function useGetData(query, pageNumber) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [books, setBooks] = useState([]);
  const [hasMore, setHasMore] = useState([]);
}
それでは、私たちのAPI呼び出しをするときだけ、私たちのquery パラメータの変更pageNumber です.インサイド、セットしたいloading to true and error to false .
useEffect(() => {
    setLoading(true);
    setError(false);
  }, [query, pageNumber]);

この番組の肉


さて、Axios経由でHTTPリクエストを行います.Axiosがリクエストをキャンセルするために使用するキャンセル変数を宣言します.これは、我々の問い合わせが変更されるたびにリクエストを作成したくないので、これは必要です.なぜなら、新しい文字が入力フィールドに入力されるたびにリクエストが行われるからです.したがって、効率の悪いコードとなる.ユーザーが入力を終えたならば、解決は要求をするだけです.Axiosは、そのようなイベントがどのように行われているのかを判断するのが簡単です.
let cancel;
    axios({
      method: "GET",
      url: "https://openlibrary.org/search.json",
      params: { q: query, page: pageNumber },
      cancelToken: new axios.CancelToken((c) => (cancel = c))
    })
      .then((res) => {
        setBooks(prevBooks => {
          return [...new Set([...prevBooks, ...res.data.docs.map(b => b.title)])]
        })
        setHasMore(res.data.docs.length > 0)
        setLoading(false)
      })
      .catch((e) => {
        if (axios.isCancel(e)) return;
        setError(true)
      });
    return () => cancel();
ご覧の通り、追加のオプションを渡す必要がありますcancelToken オプションパラメータオブジェクトの後にparam キー.Axiosがリクエストをキャンセルするために使用するCancelTokenを返します.
この鍵の一部は、私たちのミニ効果です.
 useEffect(() => {
    setBooks([])
   }, [query])
このスニペットは、ユーザーが新しいクエリを作成した後に結果リストをリセットするために必要です.そうでなければ、私たちは無限にドキュメントを追加します.
この機能のもう一つの重要な部分は私たちですcatch メソッド:
catch((e) => {
        if (axios.isCancel(e)) return;
        setError(true)
      })
if文がどのようにトリガされるかを通知します.axios.isCancel(e) is true or false . これはキー変更が検出されたかどうかを検出することに相当する.リクエストが処理され、エラーが受信された場合は、setError(true) エラー状態を更新するには
さらに別のキー部分は、クリーンアップ機能です.return () => cancel() . この機能は、Adobe ' CancelTokenオブジェクトによって返される関数を実行するために、RetrixのuseAffectフックによって提供されます.今、リクエストは中断されたフェッチで処理されるだけです.一度、ユーザーは再びタイプして、状態変化を引き起こします、要請はキャンセルされて、前処理されます.

まだもう少し肉


HTTPリクエストの結果をスキップしたことに気づいていたかもしれません.
then((res) => {
        setBooks(prevBooks => {
          return [...new Set([...prevBooks, ...res.data.docs.map(b => b.title)])]
        })
        setHasMore(res.data.docs.length > 0)
        setLoading(false)
      })
SetStateの関数バージョンを使用して、前の状態にある関数を宣言し、新しい状態を返します.返される状態は、以前の書籍の破壊された配列の破壊されたセットと、それぞれのブックタイトルフィールドが取り出された後に取り出されたドキュメントの破棄された配列です.私は知っている.
これは、私たちが繰り返し本のタイトルを持っている可能性がありますので、このように行われますSet 簡単に私たちは私たちの配列を変異のコストですべての繰り返し値をフィルタリングすることができます.したがって、この配列の浅いコピーはその整合性を維持するために必要です.新しい状態は現在、以前の本のタイトルと私たちの新しい結果です.
我々の結果があると、結果の最後に到達したかどうかチェックする時間です.そのためにsetHasMore(res.data.docs.length > 0) をtrueに評価します.どうやってこれを知るの?さて、検索されたデータはドキュメントの配列であり、もしその配列の長さが0の場合、我々は最後に到達したと仮定できます.

エーconsole.log(res.data) 検索データを表示します.

変数を返す


私たちはreturn {loading, error, books, hasMore} 我々のカスタムフックの終わりには、我々の「フロントエンド」がデータを視覚化する必要があるすべての必要な変数を返します.
これが最後ですuseGetData.js :
import { useState, useEffect } from "react";
import axios from "axios";

export default function useGetData(query, pageNumber) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [books, setBooks] = useState([]);
  const [hasMore, setHasMore] = useState(false);

  useEffect(() => {
    setBooks([])
   }, [query])

  useEffect(() => {
    setLoading(true)
    setError(false)
    let cancel;
    axios({
      method: "GET",
      url: "https://openlibrary.org/search.json",
      params: { q: query, page: pageNumber },
      cancelToken: new axios.CancelToken((c) => (cancel = c))
    })
      .then((res) => {
        setBooks(prevBooks => {
          return [...new Set([...prevBooks, ...res.data.docs.map(b => b.title)])]
        })
        console.log(res.data)
        setHasMore(res.data.docs.length > 0)
        setLoading(false)
      })
      .catch((e) => {
        if (axios.isCancel(e)) return;
        setError(true)
      });
    return () => cancel();
  }, [query, pageNumber]);

  return {loading, error, books, hasMore};
}

ステップ4 -結果を表示する


戻りましょうApp.js 次のインポートを行います.
import React, { useState, useRef, useCallback } from "react";
import useGetData from "./useGetData";
import "./styles.css";
変数を宣言しましょう
const [query, setQuery] = useState("");
const [pageNumber, setPageNumber] = useState(1);
const { books, hasMore, loading, error } = useGetData(query, pageNumber);
我々query 変数はクエリ状態を保存することができます.Then, pageNumber を初期化する.最後に、カスタムフックから取得した変数を表す破壊オブジェクトを宣言します.我々は、3を通過しなければならないことに注意してくださいquery and pageNumber 我々のフックが正しく処理されるために.
さて、次のコードを書きます.
const observer = useRef();
  const lastBookElement = useCallback(
    (node) => {
      if (loading) return;
      if (observer.current) observer.current.disconnect();
      observer.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          setPageNumber((prevPageNumber) => prevPageNumber + 1);
        }
      });
      if (node) observer.current.observe(node);
    },
    [loading, hasMore]
  );
ご覧の通り使用const observer = useRef(); 我々の結果の最後の要素が視界に入るとき、我々は引き起こされる観察者を宣言することができます.次の関数lastBookElement , USECLALKEを使用して、変更されていない限り再作成されるのを防ぐloading 国家または我々hasMore フラグが変わるので、[loading, hasMore] .
さて、usecallback hook内でHTMLノード要素を受け取ります.第一に、私たちはloading trueに評価します.つまり、現在の最終ノードを検出したくないということです.次の評価if (observer.current) observer.current.disconnect(); , 単にオブザーバーを現在の要素に切断します.その結果、ドキュメントの新しいリストが取り出されると、新しい要素が接続されます.

交差点オブザーバ


次のコードスニペットでは、参照先のノードがウィンドウに表示されているかどうかを判断し、検索結果が存在するかどうかを確認できます.
observer.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          setPageNumber((prevPageNumber) => prevPageNumber + 1);
        }
      });
オブザーバーを割り当てるnew IntersectionObserver この関数は引数として関数を受け取り、ノードエントリの配列をとり、isIntersecting , 必要な変数はどれですか.このノードを可視化すると、ページ番号を1ずつインクリメントします.

続けましょう


function handleSearch(e) {
    setQuery(e.target.value);
    setPageNumber(1);
  }
我々は今宣言handleSearch 更新する機能query and pageNumber .
最後に、HTMLコンポーネントを返しましょう.
return (
    <div className="App">
      <input type="text" value={query} onChange={handleSearch}></input>
      {books.map((book, index) => {
        if (books.length === index + 1) {
          return (
            <div ref={lastBookElement} key={book}>
              {book}
            </div>
          );
        } else {
          return (
            <div key={book}>
              <h3>{book}</h3>
            </div>
          );
        }
      })}
      {loading && (
        <div>
          <h3>Loading...</h3>
        </div>
      )}
      {error && (
        <div>
          <h3>There seems to be an error</h3>
        </div>
      )}
    </div>
  );
まず、入力要素を次のように更新します.
<input type="text" value={query} onChange={handleSearch}>
現在、値は追跡されます、そして、OnChangeメソッドは付けられました.
次に、結果をマップします.
{books.map((book, index) => {
        if (books.length === index + 1) {
          return (
            <div ref={lastBookElement} key={book}>
              {book}
            </div>
          );
        } else {
          return (
            <div key={book}>
              {book}
            </div>
          );
        }
      })}
添付の注意ref 最後の要素にある場合のみ排他的に属性を指定します.(books.length === index + 1) . そうでない場合、ref 属性.
このようにして負荷とエラーの要素を表示できます.
{loading && (
        <div>
          <h3>Loading...</h3>
        </div>
      )}
      {error && (
        <div>
          <h3>There seems to be an error</h3>
        </div>
      )}
これが最後ですApp.js :
import React, { useState, useRef, useCallback } from "react";
import useGetData from "./useGetData";
import "./styles.css";

export default function App() {
  const [query, setQuery] = useState("");
  const [pageNumber, setPageNumber] = useState(1);
  const { books, hasMore, loading, error } = useGetData(query, pageNumber);

  const observer = useRef();
  const lastBookElement = useCallback(
    (node) => {
      if (loading) return;
      if (observer.current) observer.current.disconnect();
      observer.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          setPageNumber((prevPageNumber) => prevPageNumber + 1);
        }
      });
      if (node) observer.current.observe(node);
    },
    [loading, hasMore]
  );

  function handleSearch(e) {
    setQuery(e.target.value);
    setPageNumber(1);
  }

  return (
    <div className="App">
      <input type="text" value={query} onChange={handleSearch}></input>
      {books.map((book, index) => {
        if (books.length === index + 1) {
          return (
            <div ref={lastBookElement} key={book}>
              {book}
            </div>
          );
        } else {
          return (
            <div key={book}>
              <h3>{book}</h3>
            </div>
          );
        }
      })}
      {loading && (
        <div>
          <h3>Loading...</h3>
        </div>
      )}
      {error && (
        <div>
          <h3>There seems to be an error</h3>
        </div>
      )}
    </div>
  );
}

結果


クエリの取得:

最後まで

機能デモReact Infinite Scroll - Carlos Z.