Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Server-Driven Data Tables with Laravel, Inertia and React: Filters, Sorting and Pagination in the URL

2026年10月31日に「Dev.to」が公開したITニュース「Server-Driven Data Tables with Laravel, Inertia and React: Filters, Sorting and Pagination in the URL」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

システム開発で大量の表データ(データテーブル)を扱う際、ブラウザ処理には限界がある。Laravel、Inertia、Reactを使い、検索・並べ替え・ページ送りをサーバー側で行い、その状態をURLに記録するサーバー駆動型データテーブル構築パターンを紹介。共有しやすく、高速な実装を実現する。

ITニュース解説

Webアプリケーション開発では、商品のリストや顧客情報、注文履歴など、大量のデータを一覧表示する「データテーブル」は非常に重要な機能だ。最初はデータが少なく、ブラウザ側でフィルタリングやソートを行う「クライアントサイド」の実装でも問題なく動作することが多い。しかし、データ量が数万件、数十万件と増大すると、クライアントサイドのアプローチではいくつかの問題が発生し始める。

まず「データサイズ」の問題がある。大量のデータをブラウザにまとめて送ると、ネットワーク負荷が増大し、データの表示が遅れたり、ブラウザのメモリを大量に消費して動作が重くなる。次に「状態の共有不可」の問題だ。もしフィルタリングやソートの設定がブラウザ内のデータ(Reactなどのコンポーネントの状態)にのみ保存されている場合、ユーザーがページをリロードしたり、現在の表示状態のURLを同僚に共有しようとしても、その設定は失われてしまう。同僚は、そのリンクを開いても、自分が指定したフィルタが適用されていない「デフォルトの状態」のテーブルを見ることになる。最後に「二つの真実の源」の問題がある。サーバーにはデータベースの全データがあり、ブラウザにはその一部(フィルタされたコピー)が表示される状態だと、サーバーが知る全件数とブラウザに表示されるフィルタされた件数との間で、「何ページ中何ページ目」といった情報が食い違う可能性がある。これはユーザーに混乱を与え、アプリケーションの信頼性を損ねる。

これらの問題を解決するには、「サーバー駆動型」のアプローチが有効だ。これは、フィルタリング、ソート、ページネーションといったデータの操作をすべてウェブサーバー側で行い、ブラウザのURLをテーブルの状態の「唯一の真実の源」として利用する考え方である。ユーザーがテーブル上で操作を行うと、その操作がURLのクエリパラメータに反映され、そのURLを持つリクエストをサーバーに送る。サーバーは適切なデータを処理して返し、ブラウザはそれを受け取って表示を更新する。Laravel、Inertia、Reactという技術スタックを使うことで、このサーバー駆動型テーブルを効率的に実装できる。

このアプローチの最初のステップは、ウェブサーバー側の「コントローラー」の設計だ。コントローラーは、ブラウザから送られてくるURLのクエリパラメータを受け取り、その内容を厳しく検証する役割を担う。特に重要なのは、ソート(並び替え)の対象となる列(カラム)を「ホワイトリスト」として事前に定義することだ。ホワイトリスト化とは、許可された項目だけを明示的にリストアップする方法を指す。もしユーザーが指定したソート列を無検証で直接データベースクエリに渡してしまうと、悪意のあるユーザーによる「SQLインジェクション」というセキュリティリスクが生じる可能性がある。Laravelの検証機能を使えば、Rule::in()で許可された列のみを簡単に指定できる。コントローラーは、検証されたフィルタ条件(検索キーワード、ステータス、ソート列、ソート方向、1ページあたりの表示件数など)に基づいて、データベースからデータを取得するクエリを構築する。この際、whenメソッドを使ってフィルタ条件が指定されている場合のみクエリを追加し、orderByメソッドでソートを行う。特に重要なのが、「タイブレーカー」としてのorderBy('id', $direction)である。これは、たとえば「価格」でソートする際に、同じ価格の複数の商品がある場合でも、IDというユニークな値でさらにソートすることで、ページをまたいでデータの順序が入れ替わることを防ぎ、安定したページネーションを実現する。最後に、paginateメソッドでデータをページ分割し、withQueryStringメソッドを呼び出すことで、ページネーションのリンクにも現在のフィルタ条件が自動的に含まれるようにする。これにより、ユーザーが「次のページ」をクリックしても、適用済みのフィルタ条件が維持される。取得したデータとフィルタ条件は、Inertiaを通じてフロントエンドのReactコンポーネントに渡される。

次に、フロントエンド側でウェブブラウザのURLを構築するための「純粋関数」を用意する。buildQuery関数は、現在のURLの状態とユーザーが行った変更を受け取り、次のURLの状態を生成する。この関数で最も重要なロジックは、「フィルタ条件が変更された場合に、ページ番号をリセットする」という点だ。たとえば、ユーザーが現在7ページ目を見ていて、新しい検索キーワードを入力した場合、検索結果は通常、少ないページ数に収まることが多いため、ページ番号をリセットしないと、結果が2ページしかないのに「7ページ目」を表示しようとして、空のテーブルが表示されてしまうのを防ぐ。また、値が空のフィルタ条件はURLから削除し、URLをきれいに保つ。nextSort関数は、クリックされたソート列に応じて、ソートの方向(昇順・降順)を切り替える。

そして、InertiaとReactを連携させるための「カスタムフック」を利用する。このuseTableQueryというフックは、Inertiaのrouter.getメソッドをラップして利用する。router.getは、新しいURLへのリクエストをサーバーに送る際に、いくつか便利なオプションを提供してくれる。preserveState: trueは、検索ボックスに文字を入力している途中でサーバーにリクエストが送られても、入力中の文字が消えないように、コンポーネントの状態を保持する。preserveScroll: trueは、ソートボタンをクリックした際に、ページのスクロール位置がトップに戻ってしまうのを防ぎ、ユーザー体験を向上させる。replace: trueは、ブラウザの履歴を上書きするために利用され、ユーザーが検索ボックスに文字を1文字入力するたびにブラウザの履歴が大量に残ってしまうのを防ぐ。only: ['products', 'filters']は、サーバーから送り返されるデータの中から、実際に変更があったproducts(商品データ)とfilters(フィルタ条件)の部分だけを更新するようInertiaに指示し、パフォーマンスを最適化する。さらに、検索キーワードの入力時にすぐにリクエストを送るのではなく、少し待ってからリクエストを送る「デバウンス」処理も、このフック内で実装されている。これにより、ユーザーが入力し終えるまで待ってからサーバーに問い合わせることで、不必要なリクエストを減らし、サーバーとネットワークの負荷を軽減する。

最終的に、Reactの「ページコンポーネント」でこれらの要素を組み合わせる。このコンポーネントは、Inertiaから渡された商品データとフィルタ条件を受け取る。ここで重要なのは、検索ボックスのようなユーザーが直接入力する要素以外は、ほとんどがローカルな状態(useState)を持たないことだ。ソート列やステータスフィルタ、ページ番号などは、URLから渡されたプロパティを直接表示に利用する。これにより、URLが常にテーブルの状態を正確に反映し、「唯一の真実の源」として機能するという設計思想が実現される。ユーザーがフィルタやソート、ページネーションの操作を行うと、それらのイベントハンドラは、先ほど定義したbuildQuery関数やnextSort関数を使って新しいクエリを生成し、useTableQueryフックが提供するvisitまたはvisitDebouncedメソッドを呼び出す。これにより、新しいクエリを持つリクエストがサーバーに送られ、サーバーからの応答に基づいてテーブルの表示が更新される。この一連のプロセスは、ページ全体のリロードなしに、スムーズなユーザー体験を提供する。

このサーバー駆動型データテーブルを実装する上で、いくつか注意すべき「落とし穴」がある。前述したように、ソート列の検証を怠るとセキュリティ上の問題が生じたり、意図しないソートでパフォーマンスが低下したりする。タイブレーカーとしてのIDソートを忘れると、ページネーションの順序が不安定になり、ユーザーがデータの重複や欠落を経験する可能性がある。フィルタ条件を変更した際にページ番号をリセットしないと、空のテーブルが表示されることがある。また、InertiaのpreserveState: trueを検索ボックスに適用しないと、入力途中の文字が消えてしまう問題が起きる。replace: trueを使わないとブラウザの履歴が大量に汚染されてしまう。データベースでLIKE '%term%'のような前方一致を含まないワイルドカード検索は、データ量が多いとインデックスが使えず非常に遅くなるため、必要であれば全文検索システムなどの導入を検討すべきである。最後に、select()メソッドで取得する列を明示的に指定しないと、意図しない内部情報がフロントエンドに漏洩する可能性があるため、必要なデータのみを取得するように心がけるべきだ。

まとめると、LaravelとInertiaを使ったサーバー駆動型データテーブルの実現は、URLをテーブルの状態の真実の源とし、コントローラーでの厳格な検証とソート列のホワイトリスト化を行うことが基本だ。フロントエンドでは、buildQuery関数でフィルタ変更時のページリセットなどのロジックをカプセル化し、useTableQueryフックでInertiaの動作を最適化する。さらに、タイブレーカーによる安定したソートを導入する。このアプローチは、複雑なデータグリッドライブラリに頼らず、シンプルで堅牢なデータテーブルを構築し、表示状態をリンクとして共有できるという大きな利点を提供する。

関連コンテンツ

関連IT用語