Repository navigation
Components should play nicely with Rails caching mechanisms #234
Description
Feature request
Components should provide a digest to Rails views, or be able to use Rails explicit caching directive comments or be able to provide a cache_key.
Essentially we should be able to invalidate a cache based on whether the Component class or template has changed.
Motivation
I'm just building on some previous reports primarily around where templates live and the filenames of the template/s. My concerns come down to integrating with Rails caching basics.
The issue that we run into is basically summed up by
## In any view/partial
cache[object, another_object] do
render(MyComponent.new(attribute: value))
end
In this case if the template for MyComponent is changed the cache will not be invalidated. I tried to add MyComponent to the array used by cache and provide a self.cache_key method that used ActionView::Digestor but without having some further mechanism to invalidate the digestor's cache for the template/s when the template changes this didn't work.
Rails allows for explicit directives to be used (https://guides.rubyonrails.org/caching_with_rails.html#explicit-dependencies) but by default this will only look inside app/views. You can add a view path append_view_path("app/components") but the digestor is expecting a partial and so a template filename would have to be prefixed with underscore.
The way we are currently working around this is to have an ApplicationComponent that our components inherit from and where we override ActionView::Component::Base.matching_views_in_source_location so that templates are now found in app/views/component_templates and where the files are prefixed with underscore.
def self.matching_views_in_source_location
return [] unless source_location
*path, file_name = source_location.chomp(File.extname(source_location)).split("/")
path.pop # remove components
path.concat(["views", "component_templates"])
path_to_template = path.push("_#{ file_name }").join("/")
Dir["#{ path_to_template }.*{#{ActionView::Template.template_handler_extensions.join(',')}}"]
end
Then in the above example where cache invalidation does not work via template digest changes we can do
## In any view/partial
# Template Dependency: component_templates/my_component %>
cache[object, another_object] do
render(MyComponent, attribute: value)
end
I think if AV::C is going to be core or integrate with Rails it would need a way to expose template digests nicely, or provide cache_key probably at the class level. If not then as a last resort we would need a way to use explicit directives in templates that use ActionView::Components internally.
I'd also be all 👂 to people who have solved this already or if I've missed something about the features ActionView::Component provides!
Activity
pinzonjulian commentedon Apr 18, 2020
Happy to see another report on caching! Issue 42 also opened up this question in the early days and is marked with the V3 tag.
I'll start doing some research on my end based on your research @ozzyaaron and try to see if I can take a stab at it 🤘🏽
@joelhawksley , anything I'd need to do or know before starting? All help is welcome!
joelhawksley commentedon Apr 20, 2020
@pinzonjulian I don't know if there's much I can add here, as my experience with caching is fairly minimal. Anything we can do to improve our compatibility with Rails gets a big 👍 from me 😄
dkniffin commentedon Jul 25, 2020
@pinzonjulian Not sure if you're still researching this, but I was interested and did some digging on my own. Here's what I found:
lookup_contextlookup_contextis used to look up where a given view file is, given a string. So if it's given"posts/post", it might resolve that toapp/views/posts/_post.html.erb. We can append paths to that lookup context via eg append_view_pathDigestor.digestcall, we end up here. From what I can tell, this class's job is to build up a dependency tree and convert it to a digest key. I think the only important part here (for us) is the call to DependencyTracker.find_dependencies, which lands us hererender_dependenciesand "Explicit dependencies" isexplicit_dependencies. I think we're interested in therender_dependencies, though there might be a workaround worth exploring withexplicit_dependenciesmaybe.renderkeyword, then callsadd_dependencies, which tries to determine if the method is being called with a string (eg "posts/post") or a variable (eg@post). I think this is the part we'd want to behave differently. We'd want that to support aViewComponent.newsomehow.So, in summary, I think there's two things we'd need to change to get caching working for ViewComponent:
Digestor.digestis set up to handle non-template files) tolookup_context, maybe via append_view_path.DependencyTrackerto handle aViewComponentinstance as the argument torender.I'm not sure how much of that I misunderstood, but hopefully it helps! @joelhawksley perhaps you know of someone at Github with more experience in this area that could spend some time looking over this and make a recommendation? I'd be happy to make a PR to ViewComponent and/or Rails, but I want to make sure I'm on the right track before I dive in over my head.
pinzonjulian commentedon Jul 25, 2020
Hey @dkniffin ! I've been working on and off on this and I've been documenting my progress here:
https://www.notion.so/View-Component-07d8bef1ed3342498b5805cc3166a82a
It's a little messy because I wasn't planning on sharing those but I've had very little time lately to put more time into it. Take a look at the section that says "The whole journey" for a breakdown of all the path Rails takes to cache something.
I've loved digging into this but it's been pretty rough without any guidance so I'm glad you're chiming in!
I'd like to bring my findings into Github somehow so we can build upon it together but not sure how. Any ideas @joelhawksley ?
dkniffin commentedon Jul 25, 2020
@pinzonjulian that's awesome! We both followed through it all and came to nearly the same conclusion. The bit that I think you missed was the part where Digestor.digest builds up a dependency tree. I think the
dependenciesthat's passed in there is a red herring and the actual important dependency building is in the call totreepinzonjulian commentedon Jul 25, 2020
Yeah I definitely remember the dependency tree being built but that's where I started slipping a little. I couldn't understand how to register a new location for Rails to look for dependencies. I'm curious to see if fixing that would unblock all of the possible ways for caching (there's a comprehensive list in my notion page).
I'd really like to help and keep learning about this so count me in in any further development if possible!
joelhawksley commentedon Jul 29, 2020
@dkniffin @pinzonjulian thank you for all of the time you've spent researching this issue! Perhaps one of you could provide a PR with a failing test for us to work against? I'd love to set up some time to pair on a solution.
dkniffin commentedon Jul 30, 2020
@joelhawksley I would be glad to do that, but I'm not quite sure how that test would look. This is the template I'd want to test:
and the result I'd be testing for is that if the code for
MyComponent.newchanges, the cache gets invalidated. But I'm not sure how to do that. Specifically, I guess I don't know how I'd change out the code forMyComponent(the template and/or the class).Edit: I found this test in the rails codebase that's kinda similar, but again, what I'd need to do is test that a change to the component invalidates the cache. I think that test I linked is a little different.
dkniffin commentedon Jul 31, 2020
@jhawthorn I hope you'll forgive me for summoning you to a random repo you haven't contributed to before, but I saw you made a somewhat recent change to
ActionView::DependencyTracker, so I'm hoping you might have a better idea of how this works and/or how to make a test for this and/or how we might get this working. 😅My team and I would really like to use this library, but I'm afraid that this lack of caching support may be a blocker for us
dkniffin commentedon Sep 1, 2020
@joelhawksley Any progress on this?
joelhawksley commentedon Sep 2, 2020
@dkniffin I haven't had the time, sadly. Would you be willing to write a PR with a failing test? Perhaps we could pair together on it 😄 Email me: joelhawksley@github.com
dkniffin commentedon Sep 9, 2020
@joelhawksley I definitely empathize :/ I haven't had much time to dedicate to this either. I would love to create a PR with a failing test, but I honestly don't know what that failing test would look like. I can't wrap my head around a proper way to test that changing the contents of a file busts the Rails cache, unless I use
File.openor something, but that seems brittle. But maybe it'll work? 🤷♂️However, a team member and I have thought a little about how this feature could be implemented. As stated above, I think there's two things that need to change:
app/componentsdirectory. (I still haven't thought through this a lot, but I think this should be an easy problem to solve)DependencyTrackerto handle a ViewComponent instance as the argument to render. I think that means updating ERBTracker#add_dependencies to look for a regex like/.*Component.new/.I hope to find some time at some point to test the idea in 2 out. I think that would make more sense as a PR to Rails, but I'm not sure how to have that regex not be dependent upon how ViewComponent classes are named. Maybe I could make it generically "look for any instances of a
.newcall" or something. 🤷♂️ If anyone has thoughts on that, I'm all ears!36 remaining items
joelhawksley commentedon May 7, 2025
@reeganviljoen I'm assigning this to you based on your work in #2126. Looking forward to what you come up with!
I'm also removing this from the
v4label as we'll ship it after the release, since it's non-breaking.cannikin commentedon Jan 16, 2026
I have a workaround I've been using in the meantime. I added this to my
ApplicationComponent:And then I include that as part of the key in any
cacheblock:It doesn't know anything about nested components so you'd you need to add those digests to the key as well!
rnestler commentedon Jan 26, 2026
@cannikin won't this fail to detect changes if a super class or a mix-in changes?
So having
ApplicationComponentandTileComponentwhich inherits from it:This would still return the same digest for
TileComponentcannikin commentedon Jan 26, 2026
@rnestler Yep you're right, if you're inheriting from a custom
ApplicationComponentyou'd probably want to extend this to include that and any mixins, or look throughself.ancestorsand include them all automatically, stopping at something likeViewComponent::Base.reeganviljoen commentedon Jan 29, 2026
@cannikin Thanks for the work here, it pushed me to refactor and revisit my approach.
I started with a similar, digest the component file + template files strategy, but
I’ve refactored the implementation to better match the caching behavior we want in
ViewComponent.
A simple digest like:
works well for invalidating when that specific component’s files change, but it has a
couple shortcomings for our use case:
can stay valid even if a child component’s template changes.
(.yml) files.
repeatedly re-read the same files when walking dependencies.
The current implementation addresses those by:
FooComponent.new dependencies)
reads / constant lookups during the digest run
would you mind looking at my new improved implementation
chancancode commentedon Feb 21, 2026
Hi all – came across this thread today. It seems like this is still unsolved, so I put together a solution that:
Published here: https://github.com/tildeio/view_component-cache_digest
I'll be leaving the project that motivated this patch soon, so I don't think I can sign up to shepherd this through but hopefully there is enough context in there to be useful to others who are willing to pick it up.
I believe #2126 is solving a different problem (at the component class level, how to quickly check the inputs and return a cached string from the render method). It is probably itself useful, in that it allows you to internalize the "what cache key do I put into
<%= cache %>problem to the component class, but I think because it runs into the same issues, end up working against/re-implementing a lot of the Rails cache digest logic there.I think integrating with the Rails view layer properly is a better solution to that slice of the problem, as it will work correctly in more cases/more generally. A Ruby-level DSL to express a cache key for the component can be a nice separate addition, though if view digest is working correctly it's the same as just wrapping your template with a
<%= cache %>block and have adef cache_keyon the class.joelhawksley commentedon Feb 25, 2026
@chancancode thank you for taking the time to open-source that patch and document it so thoroughly. @reeganviljoen can I interest you in looking at incorporating @chancancode's work to your PR?
reeganviljoen commentedon Feb 26, 2026
@joelhawksley when I have some time ill investigate this patch, and the way its advantages and disadvantages affect our goal of fixing cache of view component then ill work on implementing what needs to be implemented
thank you @chancancode for this its much appreciated ❤️
rud commentedon Aug 13, 2026
@reeganviljoen sorry to nudge you, but also, totally nudging you.
This would be an awesome corner to get fixed, should you ever get the energy to look at it again :)
reeganviljoen commentedon Aug 13, 2026
@rud I am still very motivated to get this across, but my main bottle neck on the pr is getting reviews from people that understand rails caching very well and getting discussion on the pr, currently my flow looks like someone adds comment/question, I either answer, or make a fix, then wait months for feedback
the more feedback and discussion I get there the more I can move that forward, so if you feel like you can have any feedback, even a small comment or your thoughts would be much appreciated as every small contribution counts
Add experimental, opt-in component caching