フックと交差点オブザーバと反応する無限スクロールの構築


何が見えるか、ウェブページで目に見えないものを知ることは、非常に役に立つ情報でありえます.あなたが怠惰な負荷の画像を表示するときに、彼らはビューから出て行くときにも、適切な分析を取得する方法については、多くのコンテンツのユーザーがあなたのブログ上で読んでください.しかし、これは通常実装するのが難しいです.歴史的には、これには専用のAPIがありませんでした. Element.getBoundingClientRect() ) 我々のアプリケーションのパフォーマンスに負の影響を与えることができる回避策のために.

導入:交差点オブザーバーAPI


私たちの目標を達成するためのより良いパフォーマンスの方法.交差点オブザーバAPIは、ブラウザの実際のビューポートにコンテキスト内のHTML要素の位置を追跡するために使用できるブラウザAPIです.「交差点オブザーバAPI」では、ターゲット要素の祖先要素またはトップレベルドキュメントのビューポートとの交点の変更を非同期的に観測する方法を提供しますMDN
私は、交差点オブザーバーを使用して反応で無限スクロールを実装することができた方法を調査したかったです.私は私がうまくいけばあなたが私が走った同じ間違いを避けるのを助けるために学んだものを要約するべきであると思いました.
あなたが快適であることは重要ですReact's ref API これはDOMノードと交差点オブザーバとの間の接続を有効にするために適用されるためです.さもなければ、反応はDOMノードにアクセスすることを計画していない宣言的なビュー層ライブラリです.

どのように、交差点オブザーバーAPIは動きますか?


交差点オブザーバーAPIの完全な理解を得るために、私はあなたがdocumentation found at MDN .
交差点オブザーバは2つの部分で働きます:特定のノードか全体的なビューポートのどちらかに接続されたオブザーバーインスタンスと、このオブザーバーへのリクエストは、その子孫の中の特定の子供をモニターするために.観察者がつくられるときに、それは一つ以上の交点エントリを受信するコールバックを伴う提供される.
簡単に言えば、DOMノードを観測し、その閾値オプションの1つ以上が満たされたときにコールバックを実行するオブザーバを作成する必要があります.閾値は0から1までの任意の比率であることができます、1は要素がビューポートで100 %であることを意味します、そして、0はビューポートから100 %です.デフォルトでは、しきい値は0に設定されます.
// Example from MDN

let options = {
  root: document.querySelector('#scrollArea') || null, // page as root
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

/* 
   options let you control the circumstances under which
   the observer's callback is invoked
*/
オブザーバーを作成したら、監視対象の要素を指定する必要があります.
let target = document.querySelector('#listItem');
observer.observe(target);
ターゲットは、IntersectionObserver , コールバックが呼び出される.コールバックはIntersectionObserverEntry オブジェクトとオブザーバー:
let callback = (entries, observer) => { 
  entries.forEach(entry => {
    // Each entry describes an intersection change for one observed
    // target element:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });


 console.log(entries, observer)
};

閾値


しきい値は、どのくらいの交差点を観測したのかIntersectionObserver
以下の画像を考えてみましょう.

まず最初に、ページ/スクロールエリアを宣言しますroot . 次に、イメージコンテナをターゲットにします.ターゲットをルートにスクロールすると、異なるしきい値を与えます.閾値は0.2のような単一の項目、または[ 0.1 , 0.2 , 0.3 , ...]のような閾値の配列である.rootプロパティは観測されている要素の祖先でなければならず、デフォルトではブラウザのビューポートでなければならないことに注意してください.
let options = {
  root: document.querySelector('#scrollArea'), 
  rootMargin: '0px',
  threshold: [0.98, 0.99, 1]
}

let observer = new IntersectionObserver(callback, options);
私たちはオブザーバーを持っていますが、まだ何も見ていません.観測を開始するには、DOMノードを観測メソッドに渡す必要があります.これは、任意の数のノードを観察することができますが、1つだけで渡すことができます.ノードを観測したくない場合、unObject ()メソッドを呼び出し、監視を停止したいノードを渡します.disconnect ()メソッドを呼び出して、任意のノードを監視するのを止めることができます.
let target = document.querySelector('#listItem');
observer.observe(target);

observer.unobserve(target);
//observing only target

observer.disconnect(); 
//not observing any node

反応する


画像のリストに無限スクロールを作成することで交差点オブザーバを実装する予定である.私たちはスーパーイージーを利用します.それはペグ化されているので、それは大きな選択です.
注意:フックを使用してデータを取得する方法を知っている必要がありますthis article . そこの良いもの!
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';

export default function App() {
  const [loading, setLoading] = useState(false);
  const [images, setImages] = useState([]);
  const [page, setPage] = useState(1);


  const fetchData = useCallback(async pageNumber => {
    const url = `https://picsum.photos/v2/list?page=${page}&limit=15`;
    setLoading(true);

    try {
      const res = await axios.get(url);
      const { status, data } = res;

      setLoading(false);
      return { status, data };
    } catch (e) {
      setLoading(false);
      return e;
    }
  }, []);

  const handleInitial = useCallback(async page => {
      const newImages = await fetchData(page);
      const { status, data } = newImages;
      if (status === 200) setImages(images => [...images, ...data]);
    },
    [fetchData]
  );

  useEffect(() => {
    handleInitial(page);
  }, [handleInitial]);

  return (
      <div className="appStyle">

      {images && (
        <ul className="imageGrid">
          {images.map((image, index) => (
            <li key={index} className="imageContainer">
              <img src={image.download_url} alt={image.author} className="imageStyle" />
            </li>
          ))}
        </ul>
      )}

      {loading && <li>Loading ...</li>}

      <div className="buttonContainer">
        <button className="buttonStyle">Load More</button>
      </div>
    </div>
  )
}
これは、アプリケーションのコアです.私たちはページをロードすることができて、それをLorem Picsum API そして、いくつかのイメージを表示します.
これはデータの取り込みを扱うことができたので良い第一歩です.次のことは、我々が我々が我々の状態に格納したイメージリストをより多くの要求にして、更新するためにコードを書く方法を考えることです.これを行うには、現在のページを取得してから1 . これはuseEffect() 私たちの呼び出しを行い、UIを更新します.
// const [page, setPage] = useState(1);
const loadMore = () => {
    setPage(page => page + 1);
    handleInitial(page);
};
偉大な、我々はupdater機能を書いている.我々は、画面上のボタンにこれを添付することができますし、私たちのための呼び出しを行います!
<div className="buttonContainer">
   <button className="buttonStyle" onClick={loadMore}>Load More</button>
</div>
ネットワークタブを開きます.あなたが適切にチェックしたならば、あなたが我々がクリックするとき、それを見るでしょうLoad More , それは実際に動作します.唯一の問題は、ページの更新された値を読むことです1 . これは面白いです、あなたはなぜそれがそうであるか疑問に思うかもしれません.単純な答えは、更新が行われたときに関数スコープ内にあり、関数が実行を終了するまで更新された状態にアクセスできないことです.これは違うsetState() コールバックが利用可能な場所.
ので、どうやってこれを解決しますか?我々は、反応を利用していますuseRef() フック.useRef() 現在の属性を参照している項目を指すオブジェクトを返します.
import React, { useRef } from "react";

const Game = () => {
  const gameRef = useRef(1);
};

const increaseGame = () => {
  gameRef.current; // this is how to access the current item
  gameRef.current++;

  console.log(gameRef); // 2, update made while in the function scope.
} 
このアプローチは、アプリケーションのデータ取り込みを適切に処理するのに役立ちます.
// Instead of const [page, setPage] = useState(1);
const page = useRef(1);

const loadMore = () => {
  page.current++;
  handleInitial(page);
};

useEffect(() => {
   handleInitial(page);
}, [handleInitial]);
今、あなたがヒットLoad More ボタンを押すと、期待通り動作します.ああ!🎉. この記事の最初の部分を考慮することができます.今、主なビジネスに、どのように我々は我々が学んだことを取ることができますIntersection Observer そして、このアプリに適用?
まず考慮すべきことはアプローチです.上記のしきい値を説明する図を使用すると、負荷がより多くのボタンが表示されると、画像を読み込むことができます.閾値を設定できます1 or 0.75 . 我々は準備しなければならないIntersection Observer 反応して.
// create a variable called observer and initialize the IntersectionObserver()
const observer = useRef(new IntersectionObserver());

/*

A couple of things you can pass to IntersectionObserver() ... 
the first is a callback function, that will be called every time
the elements you are observing is shown on the screen, 
the next are some options for the observer

*/

const observer = useRef(new IntersectionObserver(entries => {}, options)
これを行うことによってIntersectionObserver() . しかし初期化は十分ではない.反応は観察したり観察したりする必要がある.これを行うには、私たちはuseEffect() フック.しきい値を設定する1 .
// Threshold set to 1
const observer = useRef(new IntersectionObserver(entries => {}, { threshold: 1 })

useEffect(() => {
  const currentObserver = observer.current;
    // This creates a copy of the observer 
  currentObserver.observe(); 
}, []);
観察者が観察する要素を通過する必要がある.私たちのケースでは、負荷をより多くのボタンを観察したい.これに最も良いアプローチはrefを作成し、オブザーバー関数に渡します.
// we need to set an element for the observer to observer
const [element, setElement] = useState(null);

<div ref={setElement} className="buttonContainer">
  <button className="buttonStyle">Load More</button>
</div>

/*

on page load, this will trigger and set the element in state to itself, 
the idea is you want to run code on change to this element, so you 
will need this to make us of `useEffect()`

*/
だから我々は今観察したい要素を含めるためにオブザーバー機能を更新することができます
useEffect(() => {
  const currentElement = element; // create a copy of the element from state
  const currentObserver = observer.current;

  if (currentElement) {
    // check if element exists to avoid errors
    currentObserver.observe(currentElement);
  }
}, [element]);
最後に、我々のクリーンアップ機能を設定することですuseEffect() それでunobserve() コンポーネントがアンマウントします.
useEffect(() => {
  const currentElement = element; 
  const currentObserver = observer.current; 

  if (currentElement) {
    currentObserver.observe(currentElement); 
  }

  return () => {
    if (currentElement) {
      // check if element exists and stop watching
      currentObserver.unobserve(currentElement);
    }
  };
}, [element]);
我々がウェブページを見るならば、それはまだ何も変わったように見えません.さて、我々は初期化で何かをする必要があるからですIntersectionObserver() .
const observer = useRef(
  new IntersectionObserver(
    entries => {},
    { threshold: 1 }
  )
);

/*

entries is an array of items you can watch using the `IntersectionObserver()`,
since we only have one item we are watching, we can use bracket notation to
get the first element in the entries array

*/

const observer = useRef(
  new IntersectionObserver(
    entries => {
      const firstEntry = entries[0];
      console.log(firstEntry); // check out the info from the console.log()
    },
    { threshold: 1 }
  )
);
からconsole.log() , 我々は、オブジェクトが我々が見ている各項目に利用可能なを見ることができます.IsIntersectingに注意を払う必要があります場合は、負荷をより多くのボタンをビューにスクロールすると、それはtrueに変更され、ビューにないときにfalseに更新されます.
const observer = useRef(
  new IntersectionObserver(
    entries => {
      const firstEntry = entries[0];
      console.log(firstEntry);

      if (firstEntry.isIntersecting) {
        loadMore(); // loadMore if item is in-view
      }
    },
    { threshold: 1 }
  )
);
これは私たちのために動作します、あなたはウェブページをチェックし、スクロールするようにLoad More ボタンを押すとloadMore() . あなたが上下にスクロールするならば、これにはバグがあります.isIntersecting に設定されますfalse then true . あなたは多くの画像をロードするときにいつでも、それをスクロールダウンしないでください.
適切にこの仕事をするために、私たちはboundingClientRect 我々が見ているアイテムに利用できるオブジェクト.
const observer = useRef(
    new IntersectionObserver(
      entries => {
        const firstEntry = entries[0];
        const y = firstEntry.boundingClientRect.y;
        console.log(y); 
      },
      { threshold: 1 }
    )
  );
我々はその位置に興味があるLoad More ページのボタン.現在位置が以前の位置より大きいならば、我々は位置が変わったかどうかチェックする方法が欲しいです.
const initialY = useRef(0); // default position holder

const observer = useRef(
  new IntersectionObserver(
    entries => {
      const firstEntry = entries[0];
      const y = firstEntry.boundingClientRect.y;

            console.log(prevY.current, y); // check

      if (initialY.current > y) {
                console.log("changed") // loadMore()
      }

      initialY.current = y; // updated the current position
    },
    { threshold: 1 }
  )
);
この更新プログラムを使用すると、スクロールするとき、それはあなたがアップロードされ、コンテンツの中で既に利用可能なスクロールする場合は、より多くの画像とその罰金をロードする必要があります.

フルコード


import React, { useState, useEffect, useCallback, useRef } from 'react';
import axios from 'axios';

export default function App() {
  const [element, setElement] = useState(null);
  const [loading, setLoading] = useState(false);
  const [images, setImages] = useState([]);

  const page = useRef(1);
  const prevY = useRef(0);
  const observer = useRef(
    new IntersectionObserver(
      entries => {
        const firstEntry = entries[0];
        const y = firstEntry.boundingClientRect.y;

        if (prevY.current > y) {
          setTimeout(() => loadMore(), 1000); // 1 sec delay
        }

        prevY.current = y;
      },
      { threshold: 1 }
    )
  );

  const fetchData = useCallback(async pageNumber => {
    const url = `https://picsum.photos/v2/list?page=${pageNumber}&limit=15`;
    setLoading(true);

    try {
      const res = await axios.get(url);
      const { status, data } = res;

      setLoading(false);
      return { status, data };
    } catch (e) {
      setLoading(false);
      return e;
    }
  }, []);

  const handleInitial = useCallback(
    async page => {
      const newImages = await fetchData(page);
      const { status, data } = newImages;
      if (status === 200) setImages(images => [...images, ...data]);
    },
    [fetchData]
  );

  const loadMore = () => {
    page.current++;
    handleInitial(page.current);
  };

  useEffect(() => {
    handleInitial(page.current);
  }, [handleInitial]);

  useEffect(() => {
    const currentElement = element;
    const currentObserver = observer.current;

    if (currentElement) {
      currentObserver.observe(currentElement);
    }

    return () => {
      if (currentElement) {
        currentObserver.unobserve(currentElement);
      }
    };
  }, [element]);

  return (
    <div className="appStyle">
      {images && (
        <ul className="imageGrid">
          {images.map((image, index) => (
            <li key={index} className="imageContainer">
              <img src={image.download_url} alt={image.author} className="imageStyle" />
            </li>
          ))}
        </ul>
      )}

      {loading && <li>Loading ...</li>}

      <div ref={setElement} className="buttonContainer">
        <button className="buttonStyle">Load More</button>
      </div>
    </div>
  );
}
ある程度まで、IOはほとんどのブラウザ全体で使用し、サポートするのが安全です.しかし、常に使用することができますPolyfill あなたが快適でないならば.あなたはthis サポートについてもっと学ぶ

アディオス👋🏾