Railsフォームヘルパーとの組み合わせでyield内容が誤った位置に出力されるバグを修正

viewcomponent/view_component

ViewComponentがRailsのフォームヘルパー(form.labelなど)とyieldブロックを組み合わせた際に、ブロックの内容がタグの外側に出力されてしまうバグが修正されました。根本原因は@output_bufferの同期ずれにあり、captureメソッドのオーバーライドによってビューコンテキストのバッファと一致させることで解決しています。

背景

#2617で報告されたこのバグは、ViewComponentからパーシャルをレンダリングし、そのパーシャル内でRailsのフォームビルダーを使ってyieldするケースで発生していました。具体的には form.label :something do ... yield ... end という構造で、期待される出力が <label>world</label> であるにもかかわらず、実際には world <label></label> となっていました。

問題の連鎖は form_with から始まります。form_with はフォームビルダーを生成する際に @template_object としてコンポーネント自身を参照します。その後、パーシャル内でフォームビルダーが form.label :something do ... end を呼び出すと、ビルダーは @template_object.capture(builder, &block) を実行します。つまり、コンポーネントの @output_buffer に対してキャプチャを行います。

しかしRailsの Template#render_run は、パーシャルをレンダリングする際にビューコンテキストの @output_buffer新しい OutputBuffer に差し替えます。パーシャルのテンプレートコードはこの新しいバッファに書き込む一方、コンポーネントの @output_bufferrender_in 時点の古いバッファを指したままです。結果として form.label のキャプチャは空の(古い)バッファを読み取り、ブロックの出力はラベルタグで包まれることなくパーシャルのメインストリームに漏れ出してしまいます。

技術的な変更

ViewComponent::Basecapture メソッドのオーバーライドを追加し、キャプチャ前に @output_buffer をビューコンテキストの現在のバッファと同期させることで問題を解決しています。

追加されたコード(lib/view_component/base.rb):

def capture(...)
  old_output_buffer = @output_buffer
  @output_buffer = view_context.output_buffer if view_context
  super
ensure
  @output_buffer = old_output_buffer
end

この実装は3段階の手順で動作します。まず現在の @output_buffer を退避し、次にビューコンテキストが存在する場合は view_context.output_buffer(パーシャルのために差し替えられた新しいバッファ)に切り替えます。super でRailsの標準 capture を呼び出した後、ensure ブロックで元のバッファを必ず復元します。view_contextnil の場合(コンポーネントが単体テストされる状況など)は同期をスキップするため、既存の動作を壊しません。

テストには PartialWithYieldFormComponent が追加されました。コンポーネントのテンプレートから form_with でフォームを生成し、パーシャル shared/_yielding_form_partial に渡します。パーシャルは form.label :something do ... yield ... end の構造を持ち、テストは render_inline の結果から label 要素のHTMLに "world" が含まれることを検証します。

def test_render_partial_with_yield_form
  assert_includes render_inline(PartialWithYieldFormComponent.new).css("label").to_html, "world"
end

設計判断

バッファの一時的な差し替えとリストアという最小限の介入で問題を解決するアプローチが採用されました。

PRのコメントにあるとおり、@template_object がコンポーネントを指すという構造はRailsのフォームビルダーの内部実装に起因しており、ViewComponent側からその動作を変更することは困難です。そのため、フォームビルダーがコンポーネントの capture を呼び出すタイミングで、コンポーネント側がビューコンテキストの現在のバッファを参照するよう自ら合わせに行く設計が選ばれています。ensure による確実なリストアは、キャプチャ中に例外が発生した場合でも @output_buffer の状態が壊れないことを保証します。

この変更はコンポーネントの capture メソッドのみを対象としており、render_in やその他のバッファ管理ロジックには触れていません。影響範囲を最小限に抑えながら、バッファの同期ずれという根本原因を直接修正した判断といえます。

まとめ

本修正は、ViewComponentとRailsのパーシャルレンダリング機構が @output_buffer を独立して管理することで生じた、見えにくいバッファ同期の問題を解消します。capture の呼び出しスコープでビューコンテキストのバッファに追従するという局所的な修正によって、既存のコードに影響を与えることなくフォームヘルパーとyieldの組み合わせが正しく動作するようになりました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
eebf6870

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が記事全体と各セクションに適用されており、リード文、背景、技術詳細、設計判断、まとめの各要素が論理的に配置され、非常に分かりやすい。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトやGitHubのPR/Issueへのリンク記法が、ガイドライン通りに正しく使用されている。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

ViewComponentとRailsの内部的なバッファ管理という専門的なトピックを、対象読者であるエンジニアに適切な粒度と専門用語で解説している。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されており、非常に可読性が高い。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、ファイル名を含め、提供されたDiff情報と完全に一致している。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`@output_buffer`や`capture`といった技術用語がPR情報と一致しており、文脈に沿って正確に使用されている。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

バグの根本原因であるバッファの同期ずれから、`capture`メソッドのオーバーライドによる解決策まで、技術的に正確かつ論理的に説明されている。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事のすべての主張がPRのDescriptionやコードによって裏付けられており、ハルシネーション(情報の創作)は見られない。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2619)やIssue番号(#2617)などの数値・固有名詞は、PR情報と一致しており正確である。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは元のPRの主題を的確に要約しており、記事本文の内容とも完全に一致している。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容は提供されたPR情報に完全に準拠しており、根拠のない外部知識(バージョン情報など)の追加はない。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「修正されました」といった時間表現は、PRがマージ済みであることを正確に反映しており、歪曲は見られない。