9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UIから消しても通信は生きている――display:none と input[type="hidden"] が招くデッドコードの防ぎ方

9
Posted at

はじめに

サーバーサイドレンダリング+フォーム送信の構成を保守していると、こんなコードに出くわすことがあります。😇

<!-- チェックボックスはもうない。でも input[type="hidden"] だけ残っている -->
<input type="hidden" name="notifyCheck" value="true" />
// サーバー側のバリデーション
if (!form.isNotifyCheck()) {
    errors.put("notifyCheck", "再通知フラグを確認してください");
}

「再通知フラグのチェックボックス? 画面にないけど?」

調べてみると、かつてはチェックボックスが存在していた。機能廃止の際にUIからは消したが、サーバー側を触りたくなかったため <input type="hidden"> に固定値 true を入れて代替した——という経緯が見えてきました。

結果として、サーバー側のバリデーションはfalse にならず、エラーパスは誰も踏めない dead code になっていました。

この記事で扱う「hidden フィールド」は <input type="hidden"> を指します。HTML5 の hidden 属性(要素の表示を消す属性)とは別物なので、混同しないようにしてください。


先にまとめ

確認事項 結論
display:none 内の input は submit されるか される(disabled でない要素は送信対象)
<input type="hidden"> + 固定値パターンの問題 dead code 化だけでなく、仕様乖離・クライアント値信頼など複数の問題が複合する
送信自体を止めたいとき disabled 属性を付与する、または DOM から取り除く
送信されても識別しやすくしたいとき フィールド名を分離し、route パラメータでルートを確定させてから検証する

display:none は送信を止めない

HTML 仕様の定義

フォーム送信では、name を持ち、送信対象のフォーム部品であり、disabled でない要素が基本的に収集対象になります。display:none はスタイルの話であり、要素を disabled 状態にはしません。

<form method="post">
  <!-- display:none でも name 属性があれば送信される -->
  <div style="display:none">
    <input type="text" name="hidden_field" value="送られます" />
  </div>
  <button type="submit">送信</button>
</form>

このフォームをサブミットすると、hidden_field=送られます はリクエストボディに含まれます。

disabled との違い

送信を止めたいなら disabled 属性が必要です。

<!-- これは送信されない -->
<input type="text" name="skip_me" value="値" disabled />

<!-- これは送信される(display:none は関係ない) -->
<input type="text" name="send_me" value="値" style="display:none" />

display:none と disabled は全く別の概念ですが、「画面に見えていないなら送られない」という直感で混同されやすいポイントです。

visibility:hidden や height:0 も同様

visibility:hidden、opacity:0、height:0 のいずれも disabled ではないため、同じく送信対象です。CSS でどれだけ「見えなく」しても、フォーム送信の観点では意味がありません。

実際に送信される値は要素種別によって変わる

「送信対象になりうる」とはいえ、何が届くかは要素種別と入力状態に依存します。

要素 非表示状態での送信値
input[type="text"] 未入力なら空文字 ""
input[type="checkbox"] checked でなければパラメータ自体が存在しない
input[type="radio"] checked のものだけ送信(グループ全体が未選択ならパラメータなし)
input[type="hidden"] 常に value が送信される
select 選択中の option の value

「非表示 = 空文字が届く」と一律に覚えるより、「disabled でない限り送信対象になりうる、何が届くかは要素種別による」と理解する方が正確です。


<input type="hidden"> + 固定値が引き起こす問題

よくある経緯

機能廃止やUI刷新の際、「サーバー側は変えたくない」という判断で次のようなパターンが生まれます。

Before(機能廃止前):

<label>
  <input type="checkbox" name="notifyCheck" value="true" />
  再通知を希望する
</label>

After(機能廃止後):

<!-- チェックボックスを削除し、type="hidden" で固定値を送り続ける -->
<input type="hidden" name="notifyCheck" value="true" />

サーバー側は変更なし:

if (!form.isNotifyCheck()) {
    // ← 通常の画面操作では到達しない
    errors.put("notifyCheck", "再通知フラグを確認してください");
}

何が問題なのか

1. エラーパスが dead code 化する

type="hidden" は常に value="true" を送るため、通常の画面操作では isNotifyCheck() が false になることはありません。結果として if ブロックの中身は実運用上ほぼ到達不能になり、UIテストや結合テストでも見落とされやすい dead code になります。

2. 画面仕様とサーバー仕様が乖離する

仕様書や画面設計に存在しないフィールドが、リクエストには含まれている状態になります。新しくプロジェクトに入ったメンバーが「画面にないのにサーバーに届いている値があるが、これは正常なのか異常なのか」と判断できなくなります。

3. コードから意図が読めなくなる

notifyCheck という名前と「再通知フラグを確認してください」というエラーメッセージは、元々の機能の文脈がなければ何のことか分かりません。保守担当者は「このフィールドは何のためにある?消していいのか?」で詰まります。

4. クライアント送信値を業務ロジックの前提にしてしまう

<input type="hidden"> の値はブラウザの開発者ツールから書き換えられます。「クライアントが true を送ってきたから通過させる」という設計は、本来サーバー側で持つべき業務ルールの判断を、制御できない場所に委ねていることになります。

5. 連鎖的に広がる

同じフォームを使う別画面にも type="hidden" フィールドを追加する必要が出てきます。同じパターンが複数ファイルに散在し、どこが「正」か分からなくなっていきます。


ラジオボタン切り替えで顕在化する別の問題

display:none ベースのラジオ切り替えでも、同じ構造的問題が起きます。

display:none 切り替えの場合

<form method="post">
  <label><input type="radio" name="route" value="A" /> 電話番号で申し込む</label>
  <label><input type="radio" name="route" value="B" /> メールで申し込む</label>

  <div id="block-a">
    <input type="text" name="phone_number" placeholder="電話番号" />
  </div>
  <div id="block-b" style="display:none">
    <input type="text" name="email_address" placeholder="メールアドレス" />
  </div>
</form>

ルートA を選択中でも、block-b が DOM に存在する限り email_address は送信対象です(text 入力は未入力なら空文字で届く)。ルートBを選べば phone_number も届きます。

サーバーは route パラメータで選択ルートを把握しつつ、届いた値の整合性を自前でチェックする必要が生まれます。条件分岐が増え、テストケースも増えていきます。


送信を制御する2つのアプローチ

「非表示のフィールドが送られる問題」への対処は、目的によって2通りに分かれます。

アプローチ1:送信自体を止める

disabled を動的に付与する

非表示にするタイミングで disabled 属性を付与すれば、その要素は送信対象から外れます。

function switchRoute(selected) {
  document.querySelectorAll('[data-route]').forEach(block => {
    const isTarget = block.dataset.route === selected;
    block.style.display = isTarget ? '' : 'none';
    // 表示と disabled を連動させる
    block.querySelectorAll('input, select, textarea').forEach(el => {
      el.disabled = !isTarget;
    });
  });
}

HTML の表示制御と disabled の制御を連動させるのがポイントです。

なお、disabled にした要素はサーバーに届かないため、「再表示したときに入力済みの値を復元したい」ケースでは別途 state 管理が必要になります。

DOM から取り除く

非選択ブロックを DOM ごと削除しても送信対象から外れます。ただし再選択時の再描画処理が必要になるため、静的な JSP/Thymeleaf 環境では管理コストが上がります。

アプローチ2:送信されても識別しやすくする

disabled 管理が複雑になるケースでは、フィールド名にルートを含めることでサーバー側の識別を容易にする方法があります。

<div data-route="A">
  <input type="text" name="route_a_phone" placeholder="電話番号" />
</div>
<div data-route="B" style="display:none">
  <input type="text" name="route_b_email" placeholder="メールアドレス" />
</div>

サーバー側のルート判定は、route パラメータを主キーにします。フィールドの有無でルートを推定するのは、判定が不安定になるため避けてください。

// ルート判定の主キーは route パラメータ
String route = request.getParameter("route");

if ("A".equals(route)) {
    // ルートA 専用のバリデーション(route_a_phone だけを検証)
    String phone = request.getParameter("route_a_phone");
    // ...
} else if ("B".equals(route)) {
    // ルートB 専用のバリデーション(route_b_email だけを検証)
    String email = request.getParameter("route_b_email");
    // ...
}
// 非該当ルートのパラメータは無視

route_b_email が空文字で届いていても、ルートAの分岐には入らないため問題になりません。

視覚制御と送信制御は別問題

height アニメーションやアコーディオンで開閉するUIでも、DOM にはどちらのブロックも存在し続けます。視覚的な開閉とフォーム送信の制御は、別の問題として切り離して考える必要があります。

「どちらのアプローチを選ぶか」の判断基準:

方式 向いている状況
disabled 連動 シンプルなラジオ切り替えで、送信自体を確実に止めたい
フィールド名分離 + route 判定 disabled 管理が複雑、またはサーバー側で柔軟な処理が必要

4方式の比較

方式 非表示側の送信 サーバー側の判定コスト 将来の変更しやすさ
display:none のみ される 高(条件分岐増加) 低
<input type="hidden"> + 固定値 される(常に同じ値) バリデーションが死ぬ 低(dead code 化)
disabled 連動 されない 低(disabled が保証) 高
フィールド名分離 + route 判定 される場合があるが識別可 低(route で分岐) 高

SPA・モダン環境では論点が変わる

React や Vue などの SPA 環境では、フォームの送信値はコンポーネントの state で管理され、送信時に state から JSON を組み立てます。「DOM の form submit 挙動起因」で非表示フィールドが混入する問題は発生しにくくなります。

ただし、SPA でも同種の問題は別の形で出てきます。

  • state に非表示項目の値が残り続ける
  • display:none のままコンポーネントを unmount せずに隠す
  • フォームライブラリが非表示フィールドの値を保持したまま送信する

違いは「DOM の form submit が原因」ではなく、「state の保持・破棄設計や JSON シリアライズの設計が原因」になる点です。「表示されていない項目の値を送信先に渡すべきか」という問いは、環境を問わず共通して意識する必要があります。

本記事の内容は、JSP・Thymeleaf・FreeMarker のようなサーバーサイドテンプレート+jQuery 環境で、この問題がブラウザの標準フォーム送信の挙動として直接露出するという話です。


まとめ

  • display:none は見た目を変えるだけで、input の送信は止まらない。送信を止めるには disabled を付与するか、DOM から取り除く
  • <input type="hidden"> に固定値を入れてUIを「なかったこと」にすると、dead code 化・仕様乖離・クライアント値信頼の問題が複合的に起きる
  • ラジオボタン切り替えでは route パラメータでルートを確定してからフィールドを検証する。フィールドの有無でルートを推定しない
  • 視覚的な開閉とフォーム送信の制御は別問題。disabled 連動かフィールド名分離か、目的に応じて選ぶ

最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、
幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ:https://gmo-connect.jp/contactus/

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?