世界を動かす技術を、日本語で。

HTMXにおけるURL駆動型ステート

概要

  • ReactからHTMXへの移行では、複雑な状態管理が不要になる利点
  • フィルタ・ソート・ページネーション・検索状態はURLで一元管理
  • URLパラメータを唯一の真実の情報源として活用
  • HTMXのフォーム連携と自動URL同期で追加ライブラリ不要
  • シンプルかつ強力なサーバーサイド主導アーキテクチャの実現

ReactからHTMXへ:状態管理の転換

  • React では 複雑なクライアントサイド状態管理 が必要
  • HTMX 移行後は、 状態管理の多くがサーバーサイド に移行
  • フィルタ、ソート、ページネーション、検索などの 状態はURLパラメータ に集約
  • URL が唯一の真実の情報源となり、 ブックマークや共有 が容易
  • 追加の状態管理ライブラリや複雑なJavaScriptの削減

URLを状態ストアとして利用するパターン

  • 例:/?status=active&sortField=price&sortDir=desc&page=2 というURLで 全状態を表現
    • 表示中のフィルタ、ソート順、ページ番号が明示的に記録
    • URL自体が状態の完全なスナップショット
  • このアプローチにより ブックマーク・共有・リロード時の状態保持 が可能

実装パターン:3つの基本ステップ

  • サーバーが URLパラメータを読み取り、状態に応じたビューをレンダリング
  • フォームhx-include による状態の保持とHTMXリクエスト連携
  • hx-push-urlブラウザURLの自動更新 を実現

ステップ1:サーバーでURL状態を読む

  • サーバーエンドポイントで クエリパラメータ を受け取り、初期表示を制御
    • 例:sortField, sortDir, status, page などをパース
    • デフォルト値を設定し、 データ取得クエリ に反映
  • テンプレート(例:ETA)で 状態をDOMに埋め込む
    • 各フィールド値をhidden inputやselected属性で表現

ステップ2:HTMLフォームで状態を維持

  • フィルタフォーム が全ての状態を保持
    • 例:<form hx-get="/api/data" hx-push-url="true" hx-params="*">
  • hidden inputで ソート条件・ページ番号 を維持
  • ソート可能なカラム はテンプレートロジックで動的にclassや値を切替
    • sortField, sortDir を動的に切替
    • 状態反映用のCSSクラス(例:sorted)を付与
  • JavaScript不要 で状態連携をHTMLのみで実現

ステップ3:hx-push-urlによるURL自動同期

  • hx-push-url="true"hx-params="*"
    • フォームデータが自動的にURLパラメータ化
    • 履歴管理(戻る・進む)も自動化
  • 状態管理やURL更新のための追加JavaScriptは不要

本パターンの利点

  • 状態がURLに明示的 なのでテスト容易
  • ブックマークや共有 で同じ状態を再現可能
  • SEO対応 (検索エンジンが全状態をクロール可能)
  • デバッグ容易 (現状態が常にアドレスバーで可視化)
  • ブラウザの戻る・進むボタン が期待通り機能
  • クライアント側の状態管理ライブラリ不要

運用上の注意点

  • URL長制限 (約2000文字):複雑なフィルタ時は省略名やサーバー側保存も検討
  • パラメータ検証 :サーバーで必ずバリデーション・サニタイズ
  • テスト容易性 :状態が明示的なため、モック不要でテスト可能

まとめ:URL主導アーキテクチャの価値

  • URLを状態ストアとすることで、Web本来の仕組みを最大限活用
  • シンプルな構成で 複雑な多段フィルタやソート にも対応可能
  • HTMXの利点 を最大限に活かす設計
  • 状態管理ライブラリ導入前に、まず URLベースの管理 を検討推奨
  • 多くの場合、 URLだけで十分かつ優れた体験 を実現

Hackerたちの意見

これは90年代のウェブアプリケーションのクラシックなパターンだね。HTMXなしでも驚くほどうまく機能するよ。

SPAの正当な不満の一つは、このパターンが分かりにくくなったことだよね。

次は、セレクタを使ってJSをマークアップの外に出すことかな。

ただ、ここにある例のURLは、ページ2の内容が新しいアイテムが追加されるたびに変わるから、(便利に)ブックマークできないんだ。真にブックマーク可能なリストのURLを得るには、「アイテムXから始まるページ」というアプローチが一番いいと思う。Xはそのアイテムの実質的にユニークなID(例えば、プライマリキーやIDを露出しないためのタイムスタンプ)だね。

なんでダウンボートされたのか分からないけど、君の言ってることは完全に正しいよ。君が言及した方法は、トークン/カーソル/キーセットベースのページネーションって呼ばれてる。

そうだね、このエッジケースを適切に解決するのはかなりの複雑さを加えることがある(君の解決策も同じ問題があるよね? 削除や更新が混乱を招くし、技術的に)。長期間の「冪等性トークン」を使ってイベントログを指す人も見たけど、ちょっとクレイジーだよね。解決しないことを考えるのも十分に価値があるかもしれないし、むしろ直感的なUXになるかもしれない(例えば、リーダーボードのために)。

Datomicと、データベースのバージョンをURLに入れようぜ :)

でも、ここにある例のURLは、ページ2の内容が新しいアイテムが追加されるたびに変わるから、(便利に)ブックマークできないんだ。リフレッシュのたびに内容が変わるのが「(便利に)ブックマークできない」理由は何?HNのトップページ(つまり「ページ1」)はそうだけど、あれはすごく便利なブックマークだよね。

パフォーマンスもかなり良くなってるしね。

次は、URLに特定の見出しに自動スクロールするための情報が含まれることがあるって言うんだろうね。

魔法だね!

それだけじゃなくて、最近はどんな部分にも自動スクロールできるよ、「URLテキストフラグメント」を見てみて。https://www.lexo.ch/blog/2025/01/highlight-text-on-page-and-... とにかく、投稿で提案されたことは特に難しいことではないけど、誰かにとってはすべてが新しいことかもしれないね。

JSの世界はますます混乱してるよ。フォームについても似たような愚痴があるけど、なんでこんなに難しいの? 非同期関数をバックエンドにシームレスに実行できるのに、ほとんどの主要なフレームワークはURL文字列をそのまま使って、URLSearchParamsオブジェクトを自分で扱うだけなんだ。Tanstack routerは、パラメータの解析だけでなく、型付きのURLヘルパーを提供してくれるから、これが大きなメタフレームワークの目指すべきゴールだと思う。でも、シンプルさとウェブスタンダードを謳ってるsveltekitのようなツールでも、サポートはほとんどゼロだよ。非JSフレームワークでも、検索パラメータの半端なサポートに対して15行のドキュメントしかないのを見たことがある。業界は、プラットフォームを学ぶのを避けるためにかける努力の10分の1でも、これ(とフォームのポストリダイレクトゲット)を最も抵抗の少ない道にするために使った方が良くなると思う。HTMXは使ってないけど、これとそのコミュニティが物事をもっとシンプルにする再発見を推進してるのは好きだな。

Nuqsは検索パラメータの解析と管理を非常にうまくやってるよ。これはシリアライズやデシリアライズ、URLの更新を制限することが関わる複雑な問題なんだ。素晴らしいライブラリだと思う。ただ、もっとネイティブフレームワークのサポートがあればいいなとも思う。フォームも難しいのは、いろんなデータタイプやクライアントサイドの状態、(クライアント?)とサーバーのバリデーション、ネットワークの境界を越えること、コンテキストに応じたUIなどが関わるからなんだ。これらは簡単な問題じゃないよ、どんなに平均的な開発者がそれを簡単だと思いたくても。問題領域が複雑であることを受け入れる時だと思う。React Server Componentsは、URLに力を戻すための大きな一歩だと思うし、開発者がクライアントとサーバーの両方の力をフルに活用できるようにしてくれるんだ。でも、コミュニティ全体がそのメンタルモデルを複雑すぎると判断してるのが残念だね。特に、JavaScriptが有効でも無効でも動作するニュアンスのあるフォームを構築できるし、境界を越えるのもかなりスムーズに処理できるんだ。RSCを数年使ってきたけど、もう戻れないと思う。いくつかのブログ記事も書いたし、コミュニティは彼らのアイデアを理解するためにもっと時間を投資すべきだと思う。URLパラメータをうまく活用することで、UIにオブジェクトの永続性を持たせる方法についてのドラフトもあるんだ。ウェブ開発者として、もっとそれに頼るべきだし、「クライアントサイド」の状態を反映させるために使うべきだと思う。いつもではないけど、もっと頻繁にね。でも、これを完成させるのは難しいんだ。アイデアを伝えたり具体化するのが大変だから。いつかは出すつもりだよ。

Tanstack router[1] は、パラメータの解析だけでなく、型付きのURLヘルパーを提供する一級のサポートを提供している。これは大きなメタフレームワークの目標であるべきだ。 Tanstackの解決策が良いとは思えない。例えば、フォームが変更されて新しいフィールドが追加されたとき、誰かが古いHTML/JSを使って古いコードからフォームを送信したらどうなるの? Tanstackにはその状況を検出するサポートがあるのか、分析/監視/ログを取る機能(デバッグを簡単にするため)、自動的に解決する機能(可能であれば)、自動解決が不可能な場合のカスタム処理を許可する機能があるのか? ドキュメントを見る限り、そんな感じはしない。ごめん、イライラして愚痴ってるけど、これはフロントエンドの世界の古典的な問題で、すごくストレスが溜まる。バックエンドの世界では、多くの(もしかしたらほとんどの)ライブラリ/フレームワーク/プロトコルがそのためのビルトインサポートを持ってる。デフォルト値や非推奨を持つGraphQL、バージョンやスキーマ履歴、自動マイグレーションをサポートするAvroやProtobufを見てみて。フロントエンドコードでそれを手動で扱わなくて済むのはいつになるんだろう?

URLパラメータを唯一の真実のソースとして扱うのは… /?status=active&sortField=price&sortDir=desc&page=2 のようなURLは、現在のビューについてすべてを教えてくれる。唯一の真実のソースが存在するなんて、全く同意できない。パラメータ制御には(少なくとも)3つの状態レベルがあって、ライブラリがその違いを無視したり、開発者からこのニュアンスを取り除こうとするのは好きじゃない。 - 誰かが編集しているUIウィジェットの「進行中」状態(ラジオボタンから検索ボックスに入力された文字まで) - サーバーから読み込むことが望まれているパラメータのスナップショットを示す「コミット済み」状態;これはデバウンスされることもあるし、検索ボタンでトリガーされることもある - サーバーから最近読み込まれたものを示す「読み込み済み」状態;これは(おそらく)UIの非パラメータ制御部分で視覚化されるデータを駆動する もし誰かが検索バーに入力して「次のページ」をクリックしたら、彼らが入力した内容は忘れちゃうの? もしパラメータに変更をコミットした後に、以前のコミットからデータが読み込まれたらどうなるの? 変更は順番に発火するの? それとも以前のリクエストをキャンセルしたり、その結果を無視したりするべき? 誰かがリクエストが進行中の間に戻るボタンをクリックしたらどうなるの? あるいは、誰かがコミットされていない値を事前にコミットされた検索バーに入力しているときは? 読み込まれたパラメータを進行中のパラメータと区別して視覚化するにはどうすればいい? もしクエリの中には他よりもはるかに時間がかかるものがあって、それについてのガイダンスを提供したい場合は? これらの質問はアプリケーションによって異なるし、一律にはいかない。全ての人に合う解決策なんてないよ。もしこのコメントに共感するなら、直感的に必要だと思う表現力を持ったツールを選んで推奨してほしい。特にLLMの世界では、簡潔な構文や暗黙の状態管理を失う価値があるとは思えない。

確かに、シンプルな解決策はエッジケースでは完璧じゃない。シンプルさとエッジケースの完璧さのトレードオフだと思う。私の意見では、優先すべきはバックエンドでクエリを最適化して、素早くリフレッシュできるようにすることだね。読み込みが十分に早ければ、そのエッジケースが起こる可能性は低くなるから。

これらの質問はアプリケーションによって異なるし、一律にはいかない。 すべてのこれらは、ページ内状態を持たないという基本的な「要件」から来ているが、それでもウェブページがまるで持っているかのように振る舞うことを要求している。もしこの要件を取り除いたら、2000年代のウェブページのようになるだろう! そして、URLには確かに唯一の真実のソースが含まれている - 進行中のリクエストは、完全なページリロードでもない限り存在しない。

jQueryやExtJSで初期のウェブアプリを作っていたとき、似たような戦略を使ってたよ(でもHistory APIが使える前はURLハッシュを使ってた)。ページが読み込まれるときにlocation.hashから読み取って、フォームの状態が変わったら書き込む感じ。もっと複雑な状態(テーブルのレイアウトみたいな)については、JSONオブジェクトとして保存して、それを圧縮してbase64エンコードしてURLハッシュに入れてた。ユーザーには、共有が必要なときにサーバーから短縮URL(例えば、https://example.com/url/1afe9)を作成するブックマークレットを渡してた。

残念ながら、まだ多くの開発者の間では物議を醸してるよね :/

今、みんなPHPに戻る準備ができてると思う。Node.jsの上で再発明されるんじゃないかな。

Golangの上でCold Fusionに一票入れるよ。

そろそろだね。もうLotus Notesは再発明したし。

いつか、現代のインターネット接続でちゃんと設計されたSSRウェブアプリが、純粋なクライアントサイドの体験と区別がつかないってことが明らかになるといいな。ダイヤルアップモデムの時代にこの技術を使ってたけど、そこそこうまくいってたし。ボタンをクリックして0msのナビゲーションを体験することなんて、今までお客さんから指摘されたことないよ。それに、ビジネスの重要な領域ではあまり役に立たないし、神様(情報理論)を騙すことはできないからね。データが頻繁に同期してなくて、すべてのやり取りでJSONペイロードが交換されるなら、サーバーで一気にレンダリングしちゃえばいいじゃん。これで、レイテンシの議論を複雑さを売りつける人に返せるよ。単に同期の問題を後回しにしてるだけだし。

「JavaScriptだけ」のウェブ開発の時期をスキップできてよかった。俺のPostNukeスキルがまた役立つようになった。

確かに、Platformaticの人たちが先週それを発表したよね。

長年PHP開発者やってるけど、ブラウザがタダでくれるものを手に入れるために人々がどれだけ努力するかを見ると、いつも面白い(驚く)。

いろんな欠点がない、使いやすいモダンなPHPを見てみたいな。

ASP.NETとJavaEE/Springをずっと続けてきたことに、なんだか正当性を感じる。Next.jsはなんとか耐えられるけど、ウェブ開発のルーツに戻るアプローチを使ってるから、まるでJSPをもう一度やってるみたい。

実は、C#をベースにした自分のPHPを「CHP」って名付けて遊びで作り始めたんだ。今のdotnetホスティングサービス(Kestrel?)の上で動くよ。「」のコードブロック内のものを全部一つの大きなMainメソッドにインライン化して、データベースアクセスや簡単なクッキー認証のための便利なメソッドをいくつか公開してる。それに、リクエストとレスポンスのオブジェクトもね。各リクエストはJITコンパイルされて、同じパスへの将来のリクエストのためにアセンブリがメモリにキャッシュされる。キャッシュされたアセンブリより新しいソースがあれば再コンパイルするよ。サーバーを起動するときに「-ne」を引数に渡すと、.chp拡張子を落とす以外にルーティングはない。まだあまり進んでないし、2003年以来初めて自分のウェブ言語を作るためだけに意味がないけどね。

それはnextjsって呼ばれてるよ。

20年前のものを再発明したって、なんか皮肉だよね。JavaScript使いがフルサークルしてる感じ。

HTMXが、みんなにメガバイトのJSを書くのをやめさせて、昔のやり方に戻らせるために考案されたってことを考えると、これが本当に皮肉なのかは疑問だな。

そうだね、2013年か2014年くらいまでは、URL駆動の状態がすべてのやり方だったよね。cgi-binの大きな不満の一つは、状態を管理するためにURLに手動で戻す必要があったこと。あの頃は、手間をかけないcgi-binアプリが結構あったから、最初のSPAsも「URLルーティング」が登場するまでそんな感じだったんだよね。でも、これって実際にはウェブが始まった時からあった車輪を再発明してるだけなんだよね。ウェブの目的は、特定のリソースやアクション、状態にリンクできることだったのに、URLを共有する以外に何もする必要がなかったんだ。面白いのは、SPAの世界が登場した後にプログラミングを始めた世代が、2013年以前の「当たり前だったこと」を再学習しているってことだね。

正直、CGIがPythonみたいな言語からサポートされなくなったのは面白いと思ってた。実装も理解もすごく簡単だったし(実際にHTTPを理解していればだけど、そこが問題かも)、当時僕が関わっていた内部のエンタープライズアプリのニーズを超えてスケールできたんだよね。

クッキーがウェブブラウザに広く実装される前、Spinnerウェブサーバー(実質的には初期のウェブサーバーであり、開発フレームワークでもあった)が「prestate」って呼んでたものを実装してたのを覚えてる。URLの一部で、実際のパスの前にある括弧付きの部分だね。こんな感じ:http://example.com/(tables,images)/developers/