Reactでの無限スクロールと画像遅延読み込みの実装
公開: 2022-03-10HTML
Intersection Observer
APIを使用して、React機能コンポーネントに無限スクロールと画像遅延読み込みを実装する方法を学習します。 その過程で、Reactのフックのいくつかを使用する方法とカスタムフックを作成する方法を学びます。ページ付けに代わるものを探している場合は、無限スクロールを検討することをお勧めします。 この記事では、React機能コンポーネントのコンテキストでのIntersection ObserverAPIのいくつかのユースケースを検討します。 読者は、Reactの機能コンポーネントに関する実用的な知識を持っている必要があります。 Reactフックにある程度精通していることは有益ですが、いくつか見ていくので必須ではありません。
この記事の最後に、ネイティブHTMLAPIを使用して無限スクロールと画像の遅延読み込みを実装することを目標としています。 また、Reactフックについてさらにいくつかのことを学びました。 これにより、必要に応じてReactアプリケーションに無限スクロールと画像の遅延読み込みを実装できます。
始めましょう。
Reactとリーフレットを使用したマップの作成
CSVまたはJSONファイルから情報を取得することは、複雑であるだけでなく、面倒でもあります。 同じデータを視覚補助の形で表現する方が簡単です。 Shajia Abidiが、ツールLeafletの強力さ、およびさまざまな種類のマップを作成する方法について説明します。 関連記事を読む→
交差点オブザーバーAPI
MDNドキュメントによると、「Intersection Observer APIは、ターゲット要素と祖先要素またはトップレベルドキュメントのビューポートとの交差の変化を非同期的に監視する方法を提供します」。
このAPIを使用すると、無限スクロールや画像の遅延読み込みなどの優れた機能を実装できます。 交差オブザーバーは、コンストラクターを呼び出し、コールバックとオプションオブジェクトを渡すことによって作成されます。 コールバックは、 target
と呼ばれる1つの要素が、デバイスビューポートまたはroot
と呼ばれる指定された要素のいずれかと交差するたびに呼び出されます。 options引数でカスタムルートを指定するか、デフォルト値を使用できます。
let observer = new IntersectionObserver(callback, options);
APIは簡単に使用できます。 典型的な例は次のようになります。
var intObserver = new IntersectionObserver(entries => { entries.forEach(entry => { console.log(entry) console.log(entry.isIntersecting) // returns true if the target intersects the root element }) }, { // default options } ); let target = document.querySelector('#targetId'); intObserver.observe(target); // start observation
entries
は、 IntersectionObserverEntry
オブジェクトのリストです。 IntersectionObserverEntry
オブジェクトは、1つの観測されたターゲット要素の交差の変更を記述します。 コールバックはメインスレッドで実行されるため、時間のかかるタスクを処理するべきではないことに注意してください。
交差点オブザーバーAPIは、caniuseに示されているように、現在幅広いブラウザーサポートを利用しています。
APIの詳細については、リソースセクションにあるリンクを参照してください。
ここで、実際のReactアプリでこのAPIを使用する方法を見てみましょう。 私たちのアプリの最終バージョンは、無限にスクロールし、各画像が遅延して読み込まれる画像のページになります。
useEffect
フックを使用してAPI呼び出しを行う
開始するには、このURLからスタータープロジェクトのクローンを作成します。 最小限のセットアップといくつかのスタイルが定義されています。 また、スタイリングにクラスを使用するため、 public/index.html
ファイルにBootstrap
のCSSへのリンクを追加しました。
必要に応じて、新しいプロジェクトを自由に作成してください。 リポジトリをフォローする場合は、 yarn
パッケージマネージャーがインストールされていることを確認してください。 特定のオペレーティングシステムのインストール手順については、こちらをご覧ください。
このチュートリアルでは、パブリックAPIから画像を取得して、ページに表示します。 Lorem PicsumAPIを使用します。
このチュートリアルでは、エンドポイントhttps://picsum.photos/v2/list?page=0&limit=10
を使用します。これは、画像オブジェクトの配列を返します。 次の10枚の写真を取得するには、ページの値を1に変更し、次に2に変更します。
次に、アプリコンポーネントを1つずつ作成します。
src/App.js
を開き、次のコードを入力します。
import React, { useEffect, useReducer } from 'react'; import './index.css'; function App() { const imgReducer = (state, action) => { switch (action.type) { case 'STACK_IMAGES': return { ...state, images: state.images.concat(action.images) } case 'FETCHING_IMAGES': return { ...state, fetching: action.fetching } default: return state; } } const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true}) // next code block goes here }
まず、レデューサー関数imgReducer
を定義します。 このレデューサーは2つのアクションを処理します。
-
STACK_IMAGES
アクションは、images
配列を連結します。 -
FETCHING_IMAGES
アクションは、fetching
する変数の値をtrue
とfalse
の間で切り替えます。
次のステップは、このレデューサーをuseReducer
フックに接続することです。 それが完了すると、2つのことが返されます。
-
imgData
には、2つの変数が含まれています。imagesはimages
オブジェクトの配列です。fetching
は、API呼び出しが進行中であるかどうかを示すブール値です。 -
imgDispatch
は、レデューサーオブジェクトを更新するための関数です。
useReducer
フックの詳細については、Reactのドキュメントをご覧ください。
コードの次の部分は、API呼び出しを行う場所です。 次のコードをApp.js
の前のコードブロックの下に貼り付けます。
// make API calls useEffect(() => { imgDispatch({ type: 'FETCHING_IMAGES', fetching: true }) fetch('https://picsum.photos/v2/list?page=0&limit=10') .then(data => data.json()) .then(images => { imgDispatch({ type: 'STACK_IMAGES', images }) imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) }) .catch(e => { // handle error imgDispatch({ type: 'FETCHING_IMAGES', fetching: false }) return e }) }, [ imgDispatch ]) // next code block goes here
useEffect
フック内で、 fetch
APIを使用してAPIエンドポイントを呼び出します。 次に、 STACK_IMAGES
アクションをディスパッチすることにより、API呼び出しの結果で画像配列を更新します。 API呼び出しが完了すると、 FETCHING_IMAGES
アクションもディスパッチします。
コードの次のブロックは、関数の戻り値を定義します。 useEffect
フックの後に次のコードを入力します。
return ( <div className=""> <nav className="navbar bg-light"> <div className="container"> <a className="navbar-brand" href="/#"> <h2>Infinite scroll + image lazy loading</h2> </a> </div> </navv <div id='images' className="container"> <div className="row"> {imgData.images.map((image, index) => { const { author, download_url } = image return ( <div key={index} className="card"> <div className="card-body "> <img alt={author} className="card-img-top" src={download_url} /> </div> <div className="card-footer"> <p className="card-text text-center text-capitalize text-primary">Shot by: {author}</p> </div> </div> ) })} </div> </div> </div> );
画像を表示するには、 imgData
オブジェクトのimages配列にマップします。
次に、アプリを起動し、ブラウザーでページを表示します。 レスポンシブグリッドに画像がうまく表示されるはずです。
最後のビットは、Appコンポーネントをエクスポートすることです。
export default App;
この時点での対応するブランチは01-make-api-callsです。
ページがスクロールするにつれてより多くの画像を表示することで、これを拡張してみましょう。
無限スクロールの実装
ページがスクロールするにつれて、より多くの写真を表示することを目指しています。 APIエンドポイントのhttps://picsum.photos/v2/list?page=0&limit=10
page = 0&limit = 10から、新しい写真のセットを取得するには、 page
の値をインクリメントするだけでよいことがわかります。 表示する画像がなくなったときにも、これを行う必要があります。 ここでの目的のために、ページの下部に到達したときに画像が不足していることがわかります。 Intersection ObserverAPIがそれを実現するのにどのように役立つかを見てみましょう。
src/App.js
を開き、 pageReducer
の下に新しいレデューサーimgReducer
を作成します。
// App.js const imgReducer = (state, action) => { ... } const pageReducer = (state, action) => { switch (action.type) { case 'ADVANCE_PAGE': return { ...state, page: state.page + 1 } default: return state; } } const [ pager, pagerDispatch ] = useReducer(pageReducer, { page: 0 })
1つのアクションタイプのみを定義します。 ADVANCE_PAGE
アクションがトリガーされるたびに、 page
の値が1ずつ増加します。
以下に示すように、 fetch
関数のURLを更新して、ページ番号を動的に受け入れます。
fetch(`https://picsum.photos/v2/list?page=${pager.page}&limit=10`)
imgData
をpager.page
と一緒に依存関係配列に追加します。 これを行うと、 pager.page
が変更されるたびにAPI呼び出しが実行されるようになります。
useEffect(() => { ... }, [ imgDispatch, pager.page ])
API呼び出しのuseEffect
フックの後に、以下のコードを入力します。 インポート行も更新します。
// App.js import React, { useEffect, useReducer, useCallback, useRef } from 'react'; useEffect(() => { ... }, [ imgDispatch, pager.page ]) // implement infinite scrolling with intersection observer let bottomBoundaryRef = useRef(null); const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { pagerDispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [pagerDispatch] ); useEffect(() => { if (bottomBoundaryRef.current) { scrollObserver(bottomBoundaryRef.current); } }, [scrollObserver, bottomBoundaryRef]);
変数bottomBoundaryRef
を定義し、その値をuseRef(null)
に設定します。 useRef
を使用すると、変数はコンポーネントのレンダリング間で値を保持できます。つまり、変数の現在の値は、含まれているコンポーネントが再レンダリングされるときに保持されます。 その値を変更する唯一の方法は、その変数に.current
プロパティを再割り当てすることです。
この場合、 bottomBoundaryRef.current
はnull
の値で始まります。 ページレンダリングサイクルが進むにつれて、現在のプロパティをノード<div id='page-bottom-boundary'>
に設定します。
代入ステートメントref={bottomBoundaryRef}
を使用して、 bottomBoundaryRef.current
をこの代入が宣言されているdivに設定するようにReactに指示します。
したがって、
bottomBoundaryRef.current = null
レンダリングサイクルの終わりに、次のようになります。
bottomBoundaryRef.current = <div></div>
この割り当てがどこで行われるかをすぐに確認します。
次に、オブザーバーを設定するscrollObserver
関数を定義します。 この関数は、監視するDOM
ノードを受け入れます。 ここで注意すべき重要な点は、監視対象の交差点に到達するたびに、 ADVANCE_PAGE
アクションをディスパッチすることです。 その結果、 pager.page
の値が1ずつインクリメントされます。これが発生すると、依存関係としてそれを持っているuseEffect
フックが再実行されます。 この再実行により、新しいページ番号を使用してフェッチ呼び出しが呼び出されます。
イベントの行列はこんな感じ。
監視中の交差点をヒット→ADVANCE_PAGE
アクションを呼び出す→pager.page
の値を1インクリメント→フェッチ呼び出しの実行にuseEffect
フック→fetch
呼び出しを実行→返された画像をimages
配列に連結します。
useEffect
フックでscrollObserver
を呼び出して、フックの依存関係のいずれかが変更された場合にのみ関数が実行されるようにします。 useEffect
フック内で関数を呼び出さなかった場合、関数はすべてのページレンダリングで実行されます。
bottomBoundaryRef.current
は<div id="page-bottom-boundary" style="border: 1px solid red;"></div>
を参照していることを思い出してください。 scrollObserver
に渡す前に、その値がnullでないことを確認します。 そうしないと、 IntersectionObserver
コンストラクターがエラーを返します。
useEffect
フックでscrollObserver
を使用したため、終了しないコンポーネントの再レンダリングを防ぐために、 useCallback
フックでラップする必要があります。 useCallbackの詳細については、Reactのドキュメントをご覧ください。
<div id='images'>
divの後に以下のコードを入力します。
// App.js <div id='image'> ... </div> {imgData.fetching && ( <div className="text-center bg-secondary m-auto p-3"> <p className="m-0 text-white">Getting images</p> </div> )} <div id='page-bottom-boundary' style={{ border: '1px solid red' }} ref={bottomBoundaryRef}></div>
API呼び出しが開始されると、 fetching
をtrue
に設定し、「 Gettingimages」というテキストが表示されます。 終了するとすぐに、 fetching
をfalse
に設定し、テキストを非表示にします。 コンストラクターオプションオブジェクトに別のthreshold
を設定することで、境界に正確に到達する前にAPI呼び出しをトリガーすることもできます。 最後の赤い線は、ページの境界に到達したときを正確に示しています。
この時点での対応するブランチは02-infinite-scrollです。
次に、画像の遅延読み込みを実装します。
画像の遅延読み込みの実装
下にスクロールしてネットワークタブを調べると、赤い線(下部の境界)に到達するとすぐにAPI呼び出しが発生し、表示に到達していなくてもすべての画像の読み込みが開始されることがわかります。彼ら。 これが望ましくない動作になる理由はさまざまです。 ユーザーが画像を見たいと思うまで、ネットワーク通話を保存したい場合があります。 このような場合、画像を遅延してロードすることを選択できます。つまり、スクロールして表示されるまで画像をロードしません。
src/App.js
を開きます。 無限スクロール関数のすぐ下に、次のコードを入力します。
// App.js // lazy loads images with intersection observer // only swap out the image source if the new url exists const imagesRef = useRef(null); const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); useEffect(() => { imagesRef.current = document.querySelectorAll('.card-img-top'); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgData.images]);
scrollObserver
と同様に、監視するノードを受け入れる関数imgObserver
を定義します。 en.intersectionRatio > 0
で決定されるように、ページが交差点に到達すると、要素の画像ソースを交換します。 スワップを実行する前に、まず新しいイメージソースが存在するかどうかを確認することに注意してください。 scrollObserver
関数と同様に、imgObserverをuseCallback
フックでラップして、コンポーネントが再レンダリングされないようにします。
また、置換が完了すると、 img
要素の監視を停止することにも注意してください。 これは、 unobserve
メソッドを使用して行います。
次のuseEffect
フックでは、 document.querySelectorAll
を使用してページ上の.card-img-top
のクラスを持つすべての画像を取得します。 次に、各画像を繰り返し処理し、その上にオブザーバーを設定します。
useEffect
フックの依存関係としてimgData.images
を追加したことに注意してください。 これが変更されると、 useEffect
フックがトリガーされ、次にimgObserver
が各<img className='card-img-top'>
要素で呼び出されます。
以下に示すように、 <img className='card-img-top'/>
要素を更新します。
<img alt={author} data-src={download_url} className="card-img-top" src={'https://picsum.photos/id/870/300/300?grayscale&blur=2'} />
すべて<img className='card-img-top'/>
要素にデフォルトのソースを設定し、表示する画像をdata-src
プロパティに保存します。 デフォルトの画像は通常小さいサイズであるため、ダウンロードは可能な限り少なくなっています。 <img/>
要素が表示されると、 data-src
プロパティの値がデフォルトの画像に置き換わります。
下の写真では、デフォルトの灯台の画像がまだ一部のスペースに表示されています。
この時点での対応するブランチは03-lazy-loadingです。
次に、これらすべての関数を抽象化して再利用できるようにする方法を見てみましょう。
カスタムフックへのフェッチ、無限スクロール、遅延読み込みの抽象化
フェッチ、無限スクロール、画像の遅延読み込みを正常に実装しました。 同様の機能を必要とする別のコンポーネントがアプリケーションにある可能性があります。 その場合、これらの関数を抽象化して再利用できます。 別のファイル内に移動して、必要な場所にインポートするだけです。 それらをカスタムフックに変えたいと思います。
Reactのドキュメントでは、カスタムフックを、名前が"use"
で始まり、他のフックを呼び出す可能性のあるJavaScript関数として定義しています。 この例では、 useFetch
、 useInfiniteScroll
、 useLazyLoading
の3つのフックを作成します。
src/
フォルダー内にファイルを作成します。 customHooks.js
という名前を付けて、以下のコードを中に貼り付けます。
// customHooks.js import { useEffect, useCallback, useRef } from 'react'; // make API calls and pass the returned data via dispatch export const useFetch = (data, dispatch) => { useEffect(() => { dispatch({ type: 'FETCHING_IMAGES', fetching: true }); fetch(`https://picsum.photos/v2/list?page=${data.page}&limit=10`) .then(data => data.json()) .then(images => { dispatch({ type: 'STACK_IMAGES', images }); dispatch({ type: 'FETCHING_IMAGES', fetching: false }); }) .catch(e => { dispatch({ type: 'FETCHING_IMAGES', fetching: false }); return e; }) }, [dispatch, data.page]) } // next code block here
useFetch
フックは、ディスパッチ関数とデータオブジェクトを受け入れます。 ディスパッチ関数はAPI呼び出しからApp
コンポーネントにデータを渡しますが、データオブジェクトを使用するとAPIエンドポイントURLを更新できます。
// infinite scrolling with intersection observer export const useInfiniteScroll = (scrollRef, dispatch) => { const scrollObserver = useCallback( node => { new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { dispatch({ type: 'ADVANCE_PAGE' }); } }); }).observe(node); }, [dispatch] ); useEffect(() => { if (scrollRef.current) { scrollObserver(scrollRef.current); } }, [scrollObserver, scrollRef]); } // next code block here
useInfiniteScroll
フックは、 scrollRef
とdispatch
関数を受け入れます。 scrollRef
は、実装したセクションですでに説明したように、オブザーバーを設定するのに役立ちます。 ディスパッチ関数は、APIエンドポイントURLのページ番号を更新するアクションをトリガーする方法を提供します。
// lazy load images with intersection observer export const useLazyLoading = (imgSelector, items) => { const imgObserver = useCallback(node => { const intObs = new IntersectionObserver(entries => { entries.forEach(en => { if (en.intersectionRatio > 0) { const currentImg = en.target; const newImgSrc = currentImg.dataset.src; // only swap out the image source if the new url exists if (!newImgSrc) { console.error('Image source is invalid'); } else { currentImg.src = newImgSrc; } intObs.unobserve(node); // detach the observer when done } }); }) intObs.observe(node); }, []); const imagesRef = useRef(null); useEffect(() => { imagesRef.current = document.querySelectorAll(imgSelector); if (imagesRef.current) { imagesRef.current.forEach(img => imgObserver(img)); } }, [imgObserver, imagesRef, imgSelector, items]) }
useLazyLoading
フックは、セレクターと配列を受け取ります。 セレクターは画像を見つけるために使用されます。 配列を変更すると、 useEffect
フックがトリガーされ、各画像にオブザーバーが設定されます。
新しいファイルに抽出したのは、 src/App.js
にある関数と同じであることがわかります。 今の良い点は、引数を動的に渡すことができることです。 これらのカスタムフックをAppコンポーネントで使用してみましょう。
src/App.js
を開きます。 カスタムフックをインポートし、データのフェッチ、無限スクロール、画像の遅延読み込み用に定義した関数を削除します。 レデューサーとuseReducer
を使用するセクションはそのままにしておきます。 以下のコードを貼り付けます。
// App.js // import custom hooks import { useFetch, useInfiniteScroll, useLazyLoading } from './customHooks' const imgReducer = (state, action) => { ... } // retain this const pageReducer = (state, action) => { ... } // retain this const [pager, pagerDispatch] = useReducer(pageReducer, { page: 0 }) // retain this const [imgData, imgDispatch] = useReducer(imgReducer,{ images:[], fetching: true }) // retain this let bottomBoundaryRef = useRef(null); useFetch(pager, imgDispatch); useLazyLoading('.card-img-top', imgData.images) useInfiniteScroll(bottomBoundaryRef, pagerDispatch); // retain the return block return ( ... )
無限スクロールのセクションで、 bottomBoundaryRef
についてはすでに説明しました。 pager
オブジェクトとimgDispatch
関数をuseFetch
に渡します。 useLazyLoading
は、クラス名.card-img-top
を受け入れます。 に注意してください.
クラス名に含まれています。 これにより、 document.querySelectorAll
を指定する必要がなくなります。 useInfiniteScroll
は、 page
の値をインクリメントするためのref関数とdispatch関数の両方を受け入れます。
この時点での対応するブランチは04-custom-hooksです。
結論
HTMLは、優れた機能を実装するための優れたAPIを提供するのに優れています。 この投稿では、React機能コンポーネントで交差点オブザーバーを使用するのがいかに簡単かを見てきました。 その過程で、Reactのフックのいくつかを使用する方法と、独自のフックを作成する方法を学びました。
資力
- 「InfiniteScroll + Image Lazy Loading」、Orji Chidi Matthew、GitHub
- 「無限スクロール、ページ付け、または「さらに読み込む」ボタン? eコマースでのユーザビリティの調査結果」、Christian Holst、Smashing Magazine
- 「LoremPicsum」、David Marby、Nijiko Yonskai
- 「IntersectionObserverの登場」、Surma、Web Fundamentals
- 使用できますか…
IntersectionObserver
- 「IntersectionObserverAPI」、MDNWebドキュメント
- 「コンポーネントと小道具」、React
- 「
useCallback
」、React - 「
useReducer
」、React