Digest cache blocks written inside a component template - #2714
Open
erikaxel wants to merge 3 commits into
Open
Conversation
- Override digest_path_from_template in ExperimentallyCacheable so a cache block in a component's own template gets the component's digest instead of the empty one the Digestor returns for a path that resolves to no template - Leave every other template the component renders to Rails - Add regression tests for the block busting on the component's template, an ancestor, and a component it renders
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
4.15.0 frames the win as: a
<% cache %>block wrapping the component in a view is invalidated when the component changes. That works. Acacheblock inside a component's template does not — it gets no digest at all, silently.ActionView::Helpers::CacheHelper#digest_path_from_templatecallsDigestor.digest(name: template.virtual_path, ...). Inside a component render,@current_templateis the component's own template, so the name is e.g.post_component. Nothing resolves it: component templates aren't inview_paths, andCacheDigest::Resolver#find_templatesonly answers paths prefixedview_component/cache_digest/. The Digestor returns"",digest_path_from_templatefalls back to the bare virtual path, and the fragment is never invalidated. The only signal is a log line:Measured before this change:
cacheblock lives in…app/viewstemplatebdc19872c00fd6a096130a056c3b3c89""""""The same components digest fine through the synthetic path (
PostComponent.cache_digest→cb23e1bd…), so the digest exists; it's just unreachable from wherecachelooks.This isn't specific to apps that use components as their view layer, and nesting is irrelevant. The trigger is purely where the
cacheblock sits lexically — a vanilla app withapp/components/foo_component.html.erbhits it too, becauseapp/componentsisn't inview_pathseither.Fix
No Rails change needed.
ActionView::Helpers::CacheHelperis mixed into the component's own ancestor chain, andExperimentallyCacheablesits ahead of it:So the narrowest seam,
digest_path_from_template, can be overridden in the module. When the template being digested is the component's own, the digest ViewComponent already computes is substituted for the empty one the Digestor returns. Anything else — a partial rendered from within the component, for instance — falls through tosuperunchanged.The alternative is overriding
cacheitself, since its owncache_fragment_namealready acceptsdigest_path:andcachedrops it viaoptions.slice(:skip_digest). That means duplicating the helper body (including theCachingRegistry.track_cachingwrapper) to widen oneslice, which looked like the worse maintenance trade.The digest comes from the component being rendered, not from whichever class owns the template file, so a subclass that inherits its parent's template doesn't share the parent's fragment.
Two things worth settling
digest_path_from_templateis# :nodoc:. Its signature and body are identical on 7.1 throughmain, and the tests here would catch a change, but I'm happy to take this to Rails as a request to make the seam public (or to makecacheforwarddigest_path:) if you'd rather not depend on it.cacheblocks correct by default, which is probably what users expect, but it widens the feature's reach. It also interacts with Track components that haven't opted into cacheability #2713, which argues for tracking components that never opted in. Happy to rework either way.Docs
The guide didn't say component-hosted
cacheblocks were digest-aware, but it didn't say they weren't either, and "that's all that's needed for the<% cache %>block above to work" reads as covering both. Added a section that says explicitly what including the module does and doesn't do for blocks inside a component template.Tests
Fail on
main, pass here:test_a_cache_block_in_a_component_template_is_digested— asserts the exact fragment key, read fromread_fragment.action_controllerinstrumentation rather than recomputed.test_a_subclass_rendering_an_inherited_template_uses_its_own_digest— the digest follows the component being rendered, not the template's owner.test_fragment_inside_a_component_template_is_invalidated_when_its_template_changes/..._when_an_ancestor_changes/..._when_a_rendered_component_changes(integration) — the three invalidation dimensions, end to end through a request.Plus
test_digest_path_for_anything_else_is_left_to_railsandtest_fragment_inside_a_component_template_is_unaffected_by_unrelated_components, which pass either way and pin the fall-through.bundle exec rakepasses on the default Gemfile (Rails 8.1); the cacheable suites pass ongemfiles/rails_7.1.gemfile. The only failure seen was aFerrum::PendingConnectionsErrorflake inViewComponentSystemTest, which passes when run on its own and is unrelated.Related
Independent of, but in the same area as, #2709, #2711, #2712 and #2713. Only the
docs/files overlap.