Visitar URL original
Components should play nicely with Rails caching mechanisms · Issue #234 · ViewComponent/view_component · GitHub
Skip to content

Components should play nicely with Rails caching mechanisms #234

Closed

Description

@ozzyaaron

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

  1. pinzonjulian commented on Apr 18, 2020

    @pinzonjulian

    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!

  2. joelhawksley commented on Apr 20, 2020

    @joelhawksley
    Member

    @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 😄

  3. dkniffin commented on Jul 25, 2020

    @dkniffin
    Contributor

    @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:

    • Caching starts with the cache method (or it's variants, cache_if, cache_unless, etc)
    • From there, a couple steps down, there's a call to Digestor.digest, passing in a lookup_context
    • From what I can tell, lookup_context is used to look up where a given view file is, given a string. So if it's given "posts/post", it might resolve that to app/views/posts/_post.html.erb. We can append paths to that lookup context via eg append_view_path
    • Stepping back up and down into that Digestor.digest call, 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 here
    • A few more steps down and we end up here. This is pretty well documented by the "Template digest" docs here. "Implicit dependencies" is render_dependencies and "Explicit dependencies" is explicit_dependencies. I think we're interested in the render_dependencies, though there might be a workaround worth exploring with explicit_dependencies maybe.
    • render_dependencies is interesting. It splits the template file's source code based on the render keyword, then calls add_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 a ViewComponent.new somehow.

    So, in summary, I think there's two things we'd need to change to get caching working for ViewComponent:

    1. I think we need to add our ViewComponent files (at least the template, but ideally also the ruby class file, though I'm not sure if Digestor.digest is set up to handle non-template files) to lookup_context, maybe via append_view_path.
    2. We need to update DependencyTracker to handle a ViewComponent instance as the argument to render.

    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.

  4. pinzonjulian commented on Jul 25, 2020

    @pinzonjulian

    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 ?

  5. dkniffin commented on Jul 25, 2020

    @dkniffin
    Contributor

    @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 dependencies that's passed in there is a red herring and the actual important dependency building is in the call to tree

  6. pinzonjulian commented on Jul 25, 2020

    @pinzonjulian

    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!

  7. joelhawksley commented on Jul 29, 2020

    @joelhawksley
    Member

    @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.

  8. dkniffin commented on Jul 30, 2020

    @dkniffin
    Contributor

    @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:

    <%= cache do %>
      <%= render MyComponent.new %>
    <% end %>
    

    and the result I'd be testing for is that if the code for MyComponent.new changes, 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 for MyComponent (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.

  9. dkniffin commented on Jul 31, 2020

    @dkniffin
    Contributor

    @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

  10. self-assigned this
    on Aug 3, 2020
  11. dkniffin commented on Sep 1, 2020

    @dkniffin
    Contributor

    @joelhawksley Any progress on this?

  12. joelhawksley commented on Sep 2, 2020

    @joelhawksley
    Member

    @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

  13. dkniffin commented on Sep 9, 2020

    @dkniffin
    Contributor

    @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.open or 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:

    1. We need to ensure that Rails knows to look for template files in the app/components directory. (I still haven't thought through this a lot, but I think this should be an easy problem to solve)
    2. We need to update DependencyTracker to 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 .new call" or something. 🤷‍♂️ If anyone has thoughts on that, I'm all ears!

  14. 36 remaining items

  15. joelhawksley commented on May 7, 2025

    @joelhawksley
    Member

    @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 v4 label as we'll ship it after the release, since it's non-breaking.

  16. cannikin commented on Jan 16, 2026

    @cannikin

    I have a workaround I've been using in the meantime. I added this to my ApplicationComponent:

    def self.digest
      return @digest if defined?(@digest)
    
      contents = []
    
      # add the ViewComponent class file
      if (source = Object.const_source_location(name))
        file_path = source.first
        contents << File.read(file_path) if File.exist?(file_path)
      end
    
      # add template files (ERB, etc.)
      sidecar_files(%w[erb html.erb]).each do |template|
        contents << File.read(template) if File.exist?(template)
      end
    
      @digest = Digest::MD5.hexdigest(contents.join)
    end

    And then I include that as part of the key in any cache block:

    <% cache [@message, MessageComponent.digest] do %>
      <%= render MessageComponent.new(@message) %>
    <% end %>

    It doesn't know anything about nested components so you'd you need to add those digests to the key as well!

  17. rnestler commented on Jan 26, 2026

    @rnestler

    @cannikin won't this fail to detect changes if a super class or a mix-in changes?

      # add the ViewComponent class file
      if (source = Object.const_source_location(name))
        file_path = source.first
        contents << File.read(file_path) if File.exist?(file_path)
      end

    So having ApplicationComponent and TileComponent which inherits from it:

    [4] pry(main)> ApplicationComponent.digest
    => "b3c8a505bb86ed62912f6f2ac1f3f5e4"
    [5] pry(main)> TileComponent.digest
    => "9d8fba26577bb697df2cb70e3aa4a207"
    
    # changes to application_component.rb
    
    [6] pry(main)> reload!
    Reloading...
    => true
    [7] pry(main)> TileComponent.digest
    => "9d8fba26577bb697df2cb70e3aa4a207"
    [8] pry(main)> ApplicationComponent.digest
    => "ee19e48bd9d4a4b9243889f9f52a34a8"
    

    This would still return the same digest for TileComponent

  18. cannikin commented on Jan 26, 2026

    @cannikin

    @rnestler Yep you're right, if you're inheriting from a custom ApplicationComponent you'd probably want to extend this to include that and any mixins, or look through self.ancestors and include them all automatically, stopping at something like ViewComponent::Base.

  19. reeganviljoen commented on Jan 29, 2026

    @reeganviljoen
    Collaborator

    @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:

    • read the component Ruby file
    • read sidecar templates
    • hash the concatenated contents

    works well for invalidating when that specific component’s files change, but it has a
    couple shortcomings for our use case:

    • It doesn’t account for rendered child components, so a parent component cache
      can stay valid even if a child component’s template changes.
    • It misses other inputs that affect rendering, like inline templates and sidecar i18n
      (.yml) files.
    • It tends to allocate a lot (collect strings into an array, then join), and can
      repeatedly re-read the same files when walking dependencies.

    The current implementation addresses those by:

    • including a digest of the component source (Ruby + templates + i18n sidecars)
    • incorporating digests for rendered child ViewComponents (based on extracting render
      FooComponent.new dependencies)
    • reducing allocations by streaming into the digest (Digest#update) and memoizing file
      reads / constant lookups during the digest run
    • guarding against cycles in the render graph

    would you mind looking at my new improved implementation

  20. chancancode commented on Feb 21, 2026

    @chancancode
    Contributor

    Hi all – came across this thread today. It seems like this is still unsolved, so I put together a solution that:

    1. works with Rails' cache digest design instead of against it (it is supposed to be a heuristic driven static analysis)
    2. solves the known issues mentioned here
    3. provides a manual escape hatch for any other cases not covered by these

    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 a def cache_key on the class.

  21. joelhawksley commented on Feb 25, 2026

    @joelhawksley
    Member

    @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?

  22. reeganviljoen commented on Feb 26, 2026

    @reeganviljoen
    Collaborator

    @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 ❤️

  23. rud commented on Aug 13, 2026

    @rud

    @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 :)

  24. reeganviljoen commented on Aug 13, 2026

    @reeganviljoen
    Collaborator

    @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

  25. added a commit that references this issue on Oct 12, 2026
    6f6902a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions