ほんとかどうかわかんない話(React③:ルーター、無限スクロール)


ルーター


古の時代、サーバーはurlごとにhtmlファイルを返していた。
時は流れ、urlに応じてhtml中の対応コンポーネントをレンダリングする、という動作になった。それをするのがルーター。

`react-router-dom` は、React 用のルーティングライブラリです。React アプリケーションにおけるナビゲーションを管理するために使用されます。以下は、`react-router-dom` の主な特徴と機能です:

  1. デクララティブ(Declarative)なルーティング: React コンポーネントを使ってルート(rootじゃなくてroute)を定義します。Web開発におけるrouteは、アプリケーション内の異なるページやセクションへのパスを定義することを意味します。これすなわち、かつてはページ単位でパスが付いていたものが、いまではページの部品(Reactでいうコンポーネント)ごとにパスが付いているということです。これは簡単にいうけど難儀な作業なので、ルーターにやってもらいます。これにより、URL とアプリケーションのビュー間のマッピングを簡単に設定できます。

  2. 動的ルーティング: アプリケーションの(特にコンポーネントの)ライフサイクルに合わせてルートを動的に変更することができます。この文脈でのライフサイクルは、各種コンポーネントのマウント(初期化)、アップデート(更新)、アンマウント(クリーンアップ)です。ここでいう動的ルーティングとは、ページ全体を再読み込みせず、部分を読み込むことでページ遷移をスムーズにするようなことです。無限スクロールなどは最たる例ですが、古来からのやり方でリンクをクリックしたとしても、更新が部分なら動的ルーティングです。

  3. コンポーネントベース: `Route`, `Link`, `Switch` などのコンポーネントを提供し、アプリケーション内のナビゲーションを容易にします。

  4. パラメータとクエリのサポート: URL パラメータやクエリストリングを通じて、動的な情報をルートに渡すことができます。

  5. ネストされたルーティング: 親子関係にある複数のルートを定義することができます。

  6. リダイレクトとナビゲーションの制御: ユーザーを特定のルートにリダイレクトしたり、プログラム的にナビゲーションを制御する機能を提供します。

  7. ブラウザ履歴の統合: ブラウザの履歴と統合され、ブラウザの戻る/進むボタンをサポートします。

基本的な使い方

`react-router-dom` を使用する基本的なステップは以下の通りです:

  1. リンクの作成: `Link` コンポーネントを使用して、ナビゲーションリンクを作成します。

  2. リダイレクト: `Redirect` コンポーネントを使用して、特定の条件下でのリダイレクトを処理します。

これは基本的な概要に過ぎませんが、`react-router-dom` は非常に強力で柔軟なルーティングソリューションを提供し、React アプリケーションの開発において広く使われています。

Route, Link, Switch

`react-router-dom` の `Route`、`Link`、`Switch` コンポーネントは、React アプリケーションでのルーティングを管理するためによく使用される要素です。それぞれのコンポーネントについて詳しく説明します。

1. Route

`Route` コンポーネントは、特定の URL にマッチするときにレンダリングされるコンポーネントを定義します。`Route` は、指定されたパスが現在の URL と一致するかどうかをチェックし、一致する場合にのみ、そのルートのコンポーネントをレンダリングします。

<Route path="/about" component={About} />

この例では、ユーザーが "/about" というパスにナビゲートしたときにのみ、`About` コンポーネントがレンダリングされます。

2. Link

`Link` コンポーネントは、アプリケーション内でのナビゲーションリンクを作成するために使用されます。`<a>` タグと似ていますが、`Link` はページの全体的な再読み込みを引き起こさずに、アプリケーションの状態を保持しながらビューを切り替えることができます。

<Link to="/about">About</Link>

この例では、ユーザーがこのリンクをクリックすると、`/about` パスにナビゲートします。

3. Switch

`Switch` コンポーネントは、複数の `Route` の中から最初に一致したもののみをレンダリングするために使用されます。これにより、複数のルートが同時に一致するのを防ぎ、単一のルートコンポーネントだけを表示します。

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/about" component={About} />
  <Route path="/contact" component={Contact} />
</Switch>

この例では、`Switch` は URL が `/`、`/about`、`/contact

` のいずれかに一致する場合に対応するコンポーネントをレンダリングします。`exact` 属性が `Home` コンポーネントのルートに付けられているので、このルートは URL が正確に `/` である場合にのみ一致します。それ以外の場合、`/about` や `/contact` など、最初に一致するルートに基づいてコンポーネントがレンダリングされます。

`Switch` の使用は、特に複数のルートが類似したパスパターンを持つ場合に重要です。例えば、`/user/:id` と `/user/settings` というルートがある場合、`Switch` を使用しないと、`/user/settings` にアクセスした際に両方のルートが一致してしまう可能性があります。`Switch` を使うことで、最初に一致したルートのみがレンダリングされるようになります。

これらのコンポーネントを組み合わせることで、React アプリケーションにおけるナビゲーションとルーティングを効果的に管理できます。`react-router-dom` はこれらの基本的なコンポーネントに加えて、リダイレクト、ルートパラメータ、ネストされたルーティングなど、さまざまな高度な機能を提供しています。

複数のルートが同時に一致する

「複数のルートが同時に一致する」というのは、特定の URL に対して、複数の `Route` コンポーネントが条件に合致し、それらが同時にレンダリングされる状況を指します。これは主に `react-router-dom` でのルーティングを行う際に起こり得る問題です。

例を挙げて説明しましょう。以下のような `Route` コンポーネントがあるとします:

<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />

この状態でユーザーが `/about` にアクセスした場合、本来であれば `About` コンポーネントのみがレンダリングされることが期待されます。しかし、`Route` のパスはデフォルトで部分一致(partial match)を行います。これにより、`/about` は `/`(ルートパス)とも部分一致します。その結果、`Home` コンポーネントと `About` コンポーネントの両方がレンダリングされることになります。

この問題を解決するために `Switch` コンポーネントが用いられます。`Switch` は子コンポーネントとして渡された `Route` の中から、URL に最初に一致したもののみをレンダリングします。したがって、上記の例で `Switch` を使用すると、`/about` にアクセスした際には `About` コンポーネントのみがレンダリングされます。

<Switch>
  <Route path="/" component={Home} />
  <Route path="/about" component={About} />
  <Route path="/contact" component={Contact} />
</Switch>

このように、`Switch` を使用することで、複数のルートが同時に一致する問題を防ぐことができます。また、`Route` に `exact` 属性を付けることで、URL が完全に一致する場合にのみそのルートをレンダリングするように指定することもできます。

Intersection Observer API

IntersectionObserverはWeb APIの一部として、W3C (World Wide Web Consortium) によって標準化された規格です。

Intersection Observer APIは、Web開発において、ある要素が他の要素やビューポート(ブラウザの表示領域)とどのように交差しているかを非同期的に監視するためのAPIです。これにより、要素がビューポート内に入るか、または特定の他の要素と交差する時に反応することができます。これは、例えば、画像の遅延ローディング(画面に表示されるまで画像のロードを遅らせること)、インフィニティスクロール(ユーザーがページの末尾に達すると次のコンテンツを自動的に読み込むこと)、広告の表示追跡など、さまざまな用途に使用されます。

基本的な使い方

  1. Intersection Observerの作成:
    最初に、`IntersectionObserver` コンストラクタを使用してインスタンスを作成します。コンストラクタの第一引数がコールバックで、第二引数がオプションのオブジェクトです。コールバック関数は、対象の要素が観察する要素と交差した際に呼び出され、その交差に関する情報を含むIntersectionObserverEntryオブジェクトの配列を引数として受け取ります。

  2. 監視する要素の指定:
    `observe` メソッドを使用して、交差を監視したい要素を指定します。

  3. 監視の停止:
    必要がなくなったら、`unobserve` メソッドで特定の要素の監視を停止するか、`disconnect` メソッドでオブザーバーのすべての監視を停止します。

オプション

Intersection Observerの動作は、コンストラクタに渡されるオプションオブジェクトでカスタマイズできます。重要なオプションには以下のものがあります:

  • `root`: 交差をチェックする際の基準となる要素。指定しない場合はビューポートが基準になります。

  • `rootMargin`: `root` 要素のマージンをpxまたは%で指定し、監視領域を拡張します。

  • `threshold`: 交差する要素のどの割合が交差しているときにコールバックを実行するかを指定する数値または数値の配列。例えば、`0.5` は要素の50%が交差した時に通知されることを意味します。

let observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 要素がビューポートに入った時の処理
      console.log('Element is intersecting!');
    }
  });
}, {rootMargin: '0px', threshold: 0.1});

observer.observe(document.querySelector('#targetElement'));

このAPIはパフォーマンスに優れており、スクロールイベントのリスナーを使用するよりも効率的です。スクロールイベントを多用するとパフォーマンスが低下する可能性があるため、Intersection Observer APIを使うとそのような問題を回避できます。

無限スクロール

Intersection Observer APIは、Web開発で要素がビューポートに入るかどうかを非同期で監視するための強力なツールです。これを利用して、無限スクロールの機能を実装することが可能です。無限スクロールは、ユーザーがページの末尾に達すると自動的に次のコンテンツを読み込む機能です。

以下に、簡単な無限スクロールのサンプルコードを示します。このサンプルでは、スクロールがある要素(例えば、`div`や`footer`の最下部)に近づくと新しいコンテンツをロードする仕組みを実装しています。

HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Infinite Scroll Example</title>
<style>
  /* 簡単なスタイル */
  .item {
    height: 100px;
    background-color: #f0f0f0;
    margin: 10px 0;
    padding: 20px;
    border: 1px solid #ddd;
  }
</style>
</head>
<body>

<div id="container">
  <!-- ここに動的にコンテンツが追加されます -->
</div>
<div id="observer"></div> <!-- Intersection Observerが監視する要素 -->

<script src="script.js"></script>
</body>
</html>

JavaScript (script.js)

document.addEventListener('DOMContentLoaded', () => {
  let observer;
  let loadMore = (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // 要素がビューポートに入るとこのブロックが実行される
        loadItems(); // コンテンツをロードする関数
      }
    });
  };

  let loadItems = () => {
    for (let i = 0; i < 10; i++) {
      const item = document.createElement('div');
      item.classList.add('item');
      item.textContent = `アイテム ${document.querySelectorAll('.item').length + 1}`;
      document.getElementById('container').appendChild(item);
    }
  };

  observer = new IntersectionObserver(loadMore);
  observer.observe(document.getElementById('observer'));

  // 初期コンテンツのロード
  loadItems();
});

この例では、`<div id="observer"></div>`がビューポートに入ると、`loadItems`関数が呼び出され、新しいコンテンツがページに追加されます。`IntersectionObserver`のコンストラクタに渡された`loadMore`関数は、監視対象の要素がビューポートに交差するかどうかを検出し、その結果に基づいて動作します。

この基本的な例をカスタマイズして、API呼び出しを行い外部のデータソースからコンテンツを取得するように変更することもできます。無限スクロールはユーザーエクスペリエンスを向上させることができますが、パフォーマンスにも注意を払う必要があります。大量のデータをロードする場合は、適切にリソースを管理し、必要に応じてページネーションを検討することが重要です。

Reactの場合

最下部にloader要素があって、ビューポートがそいつと交差したらIntersectionObserverのコールバックが動く。オブザーバーのコールバックが動くとloadingブール値が変更されるのでuseEffectの第一引数の関数が動く。

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

const InfiniteScroll = () => {
  const [items, setItems] = useState(Array.from({ length: 20 })); // 初期アイテム
  const [loading, setLoading] = useState(false);
  const loader = useRef(null);

  useEffect(() => {
    const options = {
      root: null, // ビューポートをルートとする
      rootMargin: "20px", // ビューポートの下端から20pxの位置を閾値とする
      threshold: 1.0 // 完全に交差したときにコールバックを実行
    };

    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && !loading) {
        setLoading(true);
        setTimeout(() => { // デモのために遅延を模擬
          setItems((prev) => [...prev, ...Array.from({ length: 20 })]);
          setLoading(false);
        }, 1000);
      }
    }, options);

    if (loader.current) {
      observer.observe(loader.current);
    }

    // クリーンアップ関数
    return () => observer.disconnect();
  }, [loading]); // `loading`が変更されたときにのみ実行

  return (
    <div>
      <ul>
        {items.map((item, index) => (
          <li key={index}>アイテム {index + 1}</li>
        ))}
      </ul>
      {loading && <p>ロード中...</p>}
      {/* この要素がビューポートに入ると更にアイテムをロード */}
      <div ref={loader} style={{ height: "100px", margin: "10px 0" }}></div>
    </div>
  );
};

export default InfiniteScroll;


const [loading, setLoading] = useState(false);

このコードでは、useState を使って loading という名前の状態を作成しています。この状態は、データがロード中かどうかを示すブール値(trueまたはfalse)を持ちます。useState(false) の引数 false は、loading 状態の初期値を false に設定しています。これは、コンポーネントがマウントされた時点でデータのロードが行われていないことを意味します。

setLoading は、loading 状態を更新するために使用される関数です。この関数に新しい値を渡すことで、loading 状態を更新し、関連するコンポーネントの再レンダリングをトリガーすることができます。

たとえば、データのロードを開始する時には setLoading(true) を呼び出して loading 状態を true に設定し、ロードが完了したら setLoading(false) を呼び出して loading 状態を false に戻します。この状態の変化により、ユーザーに対してロード中であることを示したり、ロードが完了したことを知らせたりするUIの更新が可能になります。

useEffectの第二引数がloadingです。
これはコンポーネントのマウント時、及びブール値であるloadingが変化すると第一引数の関数が実行されることを意味します。
前述の通り、loadingはuseStateで状態を保持し、setLoadingでブール値をスイッチします。

このコードは、Reactで作成された無限スクロール機能を持つコンポーネントの例です。無限スクロールとは、ユーザーがページの末尾に達した時に自動的に次のコンテンツを読み込むUIパターンのことです。以下に、このコードがどのように動作するか、実行順に沿って説明します。

1. `App` コンポーネントのレンダリング

最初に、`App` コンポーネントがレンダリングされます。これはアプリケーションのメインコンポーネントで、画面のレイアウトや構成を定義します。`App` コンポーネント内で、`InfiniteScroll` コンポーネントがレンダリングされます。これにより無限スクロール機能がアプリに組み込まれます。

2. `InfiniteScroll` コンポーネントの初期状態

`InfiniteScroll` コンポーネントがマウントされると、初期状態が設定されます。`useState` フックを使用して、20個のアイテムを含む配列 (`items`) と、読み込み中かどうかを示すフラグ (`loading`) が初期化されます。

3. `useEffect` と `IntersectionObserver` の設定

`useEffect` フック内で、`IntersectionObserver` が作成されます。このオブザーバーは、ページの特定の要素(この場合は `loader` に参照される要素)がビューポートに入るかどうかを監視します。
オブザーバーは、監視対象の要素がビューポートに完全に入った時(`threshold: 1.0`)、そして `loading` フラグが `false` の場合にのみ、コールバック関数を実行します。

4. スクロールとデータの読み込み

ユーザーがページをスクロールして `loader` 要素がビューポートに入ると、コールバック関数が実行されます。この関数は `loading` フラグを `true` に設定し、`setTimeout` を使用して非同期に新しいアイテムの配列を既存のアイテムに追加します。
新しいアイテムが追加された後、`loading` フラグは再び `false` に設定され、さらにユーザーがスクロールを続けると、再び新しいアイテムが読み込まれる準備が整います。

5. クリーンアップ

コンポーネントがアンマウントされる時、`useEffect` のクリーンアップ関数が実行されます。これは、`IntersectionObserver` がもはや `loader` 要素を監視しないようにするためです。

6. UIの更新

アイテムのリスト (`items`) が更新されるたびに、`InfiniteScroll` コンポーネントは再レンダリングされ、新しいアイテムが画面に表示されます。また、`loading` フラグが `true` の間、"ロード中..." のメッセージが表示されます。

このコードは、Reactの機能を利用して、ユーザーがスクロールするたびに新しいコンテンツを動的に読み込む無限スクロール機能を実装しています。`useState` と `useEffect` フック、そして `IntersectionObserver`APIの組み合わせにより、効率的かつ効果的にこの機能を実現しています。

useRef

参考

useRef は、React コンポーネント内で参照(ref)を保持するためのフックです。これは、DOM 要素への直接的な参照を保持したい場合や、レンダリング間で値を保持しておきたいが、その値の更新が再レンダリングを引き起こしてほしくない場合に便利です。

const loader = useRef(null);

このコードでは、loader という参照を作成しています。初期値は null です。この参照は、後で DOM 要素に関連付けられることになります。具体的には、無限スクロールの機能を実装するために画面下部に近づいたときに更にアイテムを読み込むトリガーとなる要素にこの ref を設定します。

<div ref={loader} style={{ height: "100px", margin: "10px 0" }}></div>

上記の div 要素がビューポートに入ったとき(つまり、ユーザーがページを下にスクロールしてこの要素が見えるようになったとき)、IntersectionObserver によって検出され、更にアイテムの読み込みがトリガーされます。useRef を使用することで、この div 要素への参照を保持し、IntersectionObserver で監視できるようになります。

この記事が気に入ったらサポートをしてみませんか?