MUIの usePagination のロジックを読む5

目次

はじめに

この記事は、MUI が提供している usePagination というカスタムフックのコードリーディング第五回です。

過去の記事をまだご覧になっていない方は、そちらも併せてご覧ください。

前回は、siblingsStart を求める処理をコードリーディングしました。

その結果、現在ページから手前の省略記号までを表示するための値だということがわかりました。

今回は、残りの処理をコードリーディングしたいと思います。

概要

以下は、ページの配列を作っている箇所を抜粋したコードです。

今回は、前回の続きの58行目から読み進めていきます。

  const range = (start, end) => {
    const length = end - start + 1;
    return Array.from({ length }, (_, i) => start + i);
  };

  const startPages = range(1, Math.min(boundaryCount, count));
  const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count);

  const siblingsStart = Math.max(
    Math.min(
      // Natural start
      page - siblingCount,
      // Lower boundary when page is high
      count - boundaryCount - siblingCount * 2 - 1,
    ),
    // Greater than startPages
    boundaryCount + 2,
  );

  const siblingsEnd = Math.min(
    Math.max(
      // Natural end
      page + siblingCount,
      // Upper boundary when page is low
      boundaryCount + siblingCount * 2 + 2,
    ),
    // Less than endPages
    count - boundaryCount - 1,
  );

  // Basic list of items to render
  // for example itemList = ['first', 'previous', 1, 'ellipsis', 4, 5, 6, 'ellipsis', 10, 'next', 'last']
  const itemList = [
    ...(showFirstButton ? ['first'] : []),
    ...(hidePrevButton ? [] : ['previous']),
    ...startPages,

    // Start ellipsis
    // eslint-disable-next-line no-nested-ternary
    ...(siblingsStart > boundaryCount + 2
      ? ['start-ellipsis']
      : boundaryCount + 1 < count - boundaryCount
        ? [boundaryCount + 1]
        : []),

    // Sibling pages
    ...range(siblingsStart, siblingsEnd),

    // End ellipsis
    // eslint-disable-next-line no-nested-ternary
    ...(siblingsEnd < count - boundaryCount - 1
      ? ['end-ellipsis']
      : count - boundaryCount > boundaryCount
        ? [count - boundaryCount]
        : []),

    ...endPages,
    ...(hideNextButton ? [] : ['next']),
    ...(showLastButton ? ['last'] : []),
  ];

部分ごとの解説

siblingsEnd

siblingsEndはその名前のとおりsiblingsStart と対応しているものと思います。
なので、おそらく「現在ページから後ろの省略記号までを表示するための値」ということになるでしょう。

それを踏まえてコードを読んでいきましょう。

  const siblingsEnd = Math.min(
    Math.max(
      // Natural end
      page + siblingCount,
      // Upper boundary when page is low
      boundaryCount + siblingCount * 2 + 2,
    ),
    // Less than endPages
    count - boundaryCount - 1,
  );

前回と同様、Math.min()Math.max() がネストしているので、一つずつ見ていきましょう。

内側の Math.max() を仮置きしてシンプル化してみます。

  const siblingsEnd = Math.min(
    x,
    // Less than endPages
    count - boundaryCount - 1,
  );

外側のMath.min() は、x と「総ページ数 – 両端から省略記号まで表示する数 – 1」のどちらか小さい方を求めていることがわかります。

続いて内側の Math.max() です。

    Math.max(
      // Natural end
      page + siblingCount,
      // Upper boundary when page is low
      boundaryCount + siblingCount * 2 + 2,
    )

「現在ページ + siblingCount 」と「両端から省略記号まで表示する数 + siblingCount * 2 + 2」のどちらか大きい方の値を求めていることがわかります。

siblingsStart と同様、総ページ数と現在ページがともに大きい値で、かつ boundaryCount もある一定の大きさの値である場合が想定されているようです。

siblingsStart との違いは、siblingsStart「現在ページから手前の省略記号まで」を求めているのに対し、siblingsEnd「現在ページから後ろの省略記号まで」を求めているということです。

前回のように実際に設定を変えて動かしてみました。
総ページ数が30で boundaryCount=3、現在ページが 3 のときにこの値が使われることがわかりました。

内側の Math.max() の処理が紐解けたので、外側の Math.min() で置き換えた x に当てはめてみましょう。

  const siblingsEnd = Math.min(
    「現在ページから後ろの省略記号まで」,
    // Less than endPages
    count - boundaryCount - 1,
  );

siblingsEnd は、現在ページから後ろの省略記号までcount - boundaryCount - 1 のどちらか小さい方の値であることがわかりました。

つまり「現在ページから後ろの省略記号まで」と「総ページ数 – 両端から省略記号まで表示する数 – 1」のどちらか小さい方ということになります。

siblingsStart や siblingsEnd のまとめ

さて、なかなか複雑な計算が行われていましたね。

正直なところ、この算出の仕組みを100%理解できているとは言えません。

ページ数が多いときや両端からの省略記号までを表示する数が多いときなど、色々なパターンに対応するために絶妙なバランスで調整がされているように思います。

そう考えると、これを一から自前で実装するのは骨が折れますし、テストも入念に行わないといけません。

であれば、せっかくカスタムフックとしてロジック部分だけ使えるようにしてくれているので、使わせてもらうに越したことはないですよね。

私たちはこのロジックをありがたく使わせてもらい、それ以外のところに力を注いでいくことでより良いものを作っていきましょう!

これまで作成した配列の要素を結合する

さて、記事が終わるような雰囲気を出してしまいましたが、まだ続きがあります。

これまで作成したのは配列の一部分です。
この後に、それらを結合する処理が残されています。

あともう一息、頑張っていきましょう!

以下のコードでは、これまで求めてきた startPagesendPagessiblingsStart などの値を結合しています。
ページャを表示するための部品が出揃ったので、くっつけているのですね。

  // Basic list of items to render
  // for example itemList = ['first', 'previous', 1, 'ellipsis', 4, 5, 6, 'ellipsis', 10, 'next', 'last']
  const itemList = [
    ...(showFirstButton ? ['first'] : []),
    ...(hidePrevButton ? [] : ['previous']),
    ...startPages,

    // Start ellipsis
    // eslint-disable-next-line no-nested-ternary
    ...(siblingsStart > boundaryCount + 2
      ? ['start-ellipsis']
      : boundaryCount + 1 < count - boundaryCount
        ? [boundaryCount + 1]
        : []),

    // Sibling pages
    ...range(siblingsStart, siblingsEnd),

    // End ellipsis
    // eslint-disable-next-line no-nested-ternary
    ...(siblingsEnd < count - boundaryCount - 1
      ? ['end-ellipsis']
      : count - boundaryCount > boundaryCount
        ? [count - boundaryCount]
        : []),

    ...endPages,
    ...(hideNextButton ? [] : ['next']),
    ...(showLastButton ? ['last'] : []),
  ];

最終的にどのような配列ができあがるかは、実はもうすでに70行目のコメントで示されています。

  // for example itemList = ['first', 'previous', 1, 'ellipsis', 4, 5, 6, 'ellipsis', 10, 'next', 'last']

それを踏まえて見ていきましょう。

72〜74行目は、「最初に戻る」「1ページ戻る」「1ページ目から手前の省略記号まで」の部品です。

    ...(showFirstButton ? ['first'] : []),
    ...(hidePrevButton ? [] : ['previous']),
    ...startPages,

showFirstButton hidePrevButton で、ボタンを表示するかどうかを切り替えていることを確認してください。

76〜85行目は、「手前の省略記号」「siblingsStart から siblingsEnd まで」の部品です。

    // Start ellipsis
    // eslint-disable-next-line no-nested-ternary
    ...(siblingsStart > boundaryCount + 2
      ? ['start-ellipsis']
      : boundaryCount + 1 < count - boundaryCount
        ? [boundaryCount + 1]
        : []),

    // Sibling pages
    ...range(siblingsStart, siblingsEnd),

「手前の省略記号」は、三項演算子がネストして少しわかりにくいですね。
このようなときも、ひるまずに落ち着いて読んでいきましょう。

最初の siblingsStart > boundaryCount + 2 の条件は、siblingsStart が「両端から省略記号までの数 + 2」の値より大きいかを判定しています。これが true のとき、省略記号を表示するための 'start-ellipsis' となります。

boundaryCount=1 のとき、boundaryCount + 2 = 3 となります。
siblingsStart=4 のとき、2〜3ページ目が省略表示となるため、'start-ellipsis' となります。

次の三項演算子boundaryCount + 1 < count - boundaryCount の条件は、boundaryCount + 1 の値を補うための処理です。

siblingsStart の値は、仕組み的にどんなに小さくても必ずboundaryCount + 2 の値になります。
boundaryCount=1 のとき、siblingsStart の最小値は 3 になります。

このとき、2ページ目が歯抜けの状態になってしまうので、boundaryCount + 1 で補われる形になります。
その状態を判定するための条件が boundaryCount + 1 < count - boundaryCount ということですね。

87〜93行目は、「後ろの省略記号」の部品です。

    // End ellipsis
    // eslint-disable-next-line no-nested-ternary
    ...(siblingsEnd < count - boundaryCount - 1
      ? ['end-ellipsis']
      : count - boundaryCount > boundaryCount
        ? [count - boundaryCount]
        : []),

「手前の省略記号」のときと同様、省略が必要な場合は 'end-ellipis' となります。

ネストした三項演算子の箇所も考え方は同じです。

siblingsEnd の値は、仕組み的にどんなに大きくても必ずcount - boundaryCount - 1 の値になります。
count=10 boundaryCount=1 のとき、siblingsEnd の最大値は 8 になります。

このとき、9ページ目が歯抜けの状態になってしまうので、count - boundaryCount で補われる形になります。
その状態を判定するための条件が count - boundaryCount > boundaryCount ということですね。

95〜97行目。
「後ろの省略記号から最後のページまで」「1ページ進む」「最後に進む」の部品をくっつけておしまいです。

    ...endPages,
    ...(hideNextButton ? [] : ['next']),
    ...(showLastButton ? ['last'] : []),

おわりに

いかがだったでしょうか?

最後は駆け足になってしまいましたが、これで usePagination の処理を一通りコードリーディングすることができました。

今回はページャを作るための処理を知りたかったので、コードリーディングをしてみました。
ですが、カスタムフックとしてロジックが閉じ込められているので、本来は中身を意識せずに使うことが可能です。

usePagination フックから返される配列を自分のアプリケーションに組み込むことで、ページの省略表示などの面倒な部分を考えずにページャを実装できるということです。

さて、5回にわたって連載してきたコードリーディングですが、今回で最終回となります。

コードリーディングをする際に一番大事なことをお伝えしたいと思います。

それは「ひるまない」ことです。

複雑でよくわからないコードが目の前に立ちはだかったとしても、ひるまないでください。

ひるんで頭が真っ白になってしまうと、読めるものも読めなくなってしまいます。

私はコードリーディングの記事を書こうと思い立ちました。
しかし、初めてこの usePagination のロジックを見たとき、実は一度ひるみました笑

「うっ。。わからん。。」とひるんでしまい、その日はそっとタブを閉じました。

ですが、後日クリアになった頭でもう一度ゆっくり立ち向かったことで、100%とは言えませんがコードを読みきることができ、こうして記事を書くことができました。

プログラミングを学習していると、次々と難しいコードに出会うことでしょう。

そのときは「ひるまない」ということをぜひ思い出してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

自分らしく力を発揮できて、自信を持って生きられる人を増やすための活動をしています。

コメント

コメントする

目次