スロット述語が引き起こすコンテンツキャッシュ汚染の修正

viewcomponent/view_component

render_in 実行前にスロット述語(header? など)を呼び出すと、content のキャッシュが汚染され、レンダリング時に渡したコンテンツブロックが無視されるバグが修正されました。

背景

render_in の呼び出し前にスロット述語を評価すると、コンテンツブロックがサイレントに無視されるという問題がありました。この問題は、コンポーネントのスロット構成をヘルパーで先に組み立て、後からコンテンツブロック付きでレンダリングするパターンで発生します。

問題の根本は __vc_get_slot の実装にありました。スロットの値を取得する際に、コンテンツ定義のスロットが確実に読み込まれるよう、content を先行評価します。

def __vc_get_slot(slot_name)
  content unless defined?(@__vc_content_evaluated) && @__vc_content_evaluated
  ...
end

この先行評価によって @__vc_content にキャッシュが書き込まれます。render_in はリセット処理として @__vc_content_evaluated = false を実行しますが、キャッシュ本体である @__vc_content はクリアしていませんでした。その結果、render_in に渡されたコンテンツブロックは一切評価されず、古いキャッシュが返り続けます。

以下のような実用的なコードで容易に再現できます。column?(:options) の呼び出しが content の先行評価を引き起こし、その後の render に渡したブロックの内容が消える、という無音の失敗を招いていました。

def my_table
  Ui::Table.new.tap do |table|
    table.with_column "Name"
    table.with_column :options unless table.column?(:options) # ここで content が評価される
  end
end
<%= render my_table do %>
  <%= render partial: "row" %>  <%# ブロックの内容がサイレントに無視される %>
<% end %>

技術的な変更

修正は lib/view_component/base.rbrender_in メソッドへの1行追加に集約されます。@__vc_content_evaluated のリセットと合わせて、@__vc_content 自体も削除することでキャッシュを完全に無効化します。

変更前:

@__vc_content_evaluated = false
@__vc_render_in_block = block

変更後:

@__vc_content_evaluated = false
remove_instance_variable(:@__vc_content) if defined?(@__vc_content)
@__vc_render_in_block = block

remove_instance_variable を使っているのは、@__vc_content が未定義の場合に NameError が発生するためです。defined? で存在確認してから削除する形が採られています。

リグレッションテストも追加されました。SlotWithContentBlockComponent という専用のテスト用コンポーネントを新設し、スロット述語を render_in より前に呼び出してからコンテンツブロック付きでレンダリングし、ブロックの内容が正しく描画されることを検証します。

class SlotWithContentBlockComponent < ViewComponent::Base
  renders_one :header

  def call
    out = +""
    out << content_tag(:div, header, class: "header") if header?
    out << content_tag(:div, content, class: "body") if content?
    out.html_safe
  end
end
def test_slot_predicate_before_render_does_not_poison_content_cache
  component = SlotWithContentBlockComponent.new
  component.with_header { "My Header" }

  # render_in 前にスロット述語を呼び出してキャッシュを汚染する
  assert component.header?

  render_inline(component) do
    "Body content from block"
  end

  assert_selector(".header", text: "My Header")
  assert_selector(".body", text: "Body content from block")
end

設計判断

キャッシュの「フラグのみリセット」から「値も含めた完全削除」へという方針が採られました。

これまでの設計では、@__vc_content_evaluated(評価済みフラグ)と @__vc_content(キャッシュ値)の2つのインスタンス変数でキャッシュ状態を管理していました。render_in はフラグだけをリセットしていたため、キャッシュ値と状態フラグの間に不整合が生じる余地がありました。今回の修正はその不整合を根本から排除する形です。

nil を代入するのではなく remove_instance_variable で変数ごと削除している点も注目に値します。@__vc_contentnil が入った状態と変数が存在しない状態を区別しているコードが他に存在する場合、nil 代入では代替できないため、変数の完全な削除が選ばれています。

まとめ

本PRは1行の追加で長年潜在していたサイレント障害を修正しています。コンテンツキャッシュの「フラグ」と「値」を常にセットで管理するという原則が徹底されたことで、スロット述語の呼び出しタイミングに依存しない堅牢なキャッシュ無効化が実現されました。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
4cca6c9f

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト、PR番号のリンク記法ともに正しく使用されています。

対象読者への適合性 ✓ PASS

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

ViewComponentの内部実装に関するトピックを、専門用語を適切に用いながら解説しており、対象読者であるエンジニアにとって適切な技術レベルです。

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

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

各セクションが総論→各論の構成になっており、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が非常によく守られています。これにより、高い可読性を実現しています。

Diff内容との照合 ✓ PASS

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

PRのDescriptionやDiffからコードを正確に引用しています。変更点、再現コード、テストコードのすべてが元の情報と一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「キャッシュ汚染」「スロット述語」「サイレント障害」など、技術的な内容を的確に表現する用語が選択されており、正確です。

説明の技術的正確性 ✓ PASS

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

キャッシュが汚染されるメカニズムと、その解決策についての技術的な説明は、PRの内容と一致しており、論理的かつ正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiffのコードから裏付けられています。「設計判断」セクションはコードの意図を深く考察していますが、PR情報から逸脱した憶測や創作はなく、ハルシネーションは見られません。

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

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

PR番号(#2603)やコード内の固有名詞(インスタンス変数名、メソッド名など)はすべて正確です。

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

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

記事のタイトルはPRのタイトル(Fix stale content cache when slots are accessed before render_in)の内容を正確かつ分かりやすく要約しています。

外部知識の正確性 ✓ PASS

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

PR情報に記載のないバージョン情報やリリース予定などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

記事内の時間表現はPR情報と矛盾していません。「長年潜在していた」という表現は、この種のバグの性質を考慮すると妥当な範囲です。