Caching
Experimental
Since 4.14.0
This API is experimental. It may change or be removed in a non-major release. Please share feedback in #234.
Rails computes a digest for every template from its source and from the templates it renders. That digest is mixed into the key of every <% cache %> block in the template, so editing a partial invalidates the caches of everything that renders it.
Components are invisible to that mechanism.
<% cache @post do %>
<%= render PostComponent.new(post: @post) %>
<% end %>
Editing PostComponent’s template, Ruby class, or sidecar files doesn’t invalidate the fragment, so the stale markup is served until the cache is cleared by hand.
Opting in
Include ViewComponent::ExperimentallyCacheable in each component that should participate in caching:
class PostComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable
def initialize(post:)
@post = post
end
end
That’s all that’s needed for the <% cache %> block above to work. The component is registered with Rails’ digest tree, and the fragment is invalidated when the component’s template, Ruby class, sidecar files, superclasses, child components, or rendered partials change, including components and partials rendered from an inline template or a #call method.
Self-caching
To have a component cache its own output without needing a cache block, use cache_on to declare methods used for the component’s cache key.
class PostComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable
cache_on :post
def initialize(post:)
@post = post
end
private
attr_reader :post
end
Every call site is now cached automatically:
<%= render PostComponent.new(post: @post) %>
Which is equivalent to writing:
<% cache [@post, PostComponent.cache_digest] do %>
<%= render PostComponent.new(post: @post) %>
<% end %>
The cache key combines:
- the component’s virtual path
- its digest, computed by Rails’
ActionView::Digestor - the requested format and variant
- the current
I18n.locale - the values returned by the
cache_onmethods
Caching is skipped unless perform_caching is enabled on the controller, matching the behavior of Rails’ cache helper.
To cache only some renders, pass if: or unless:. Both accept a method name or a proc evaluated on the component:
class PostComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable
cache_on :post, unless: -> { post.draft? }
def initialize(post:)
@post = post
end
private
attr_reader :post
end
Drafts now render every time, while published posts are cached. Note that this controls whether the cache is used, not what goes into the key: a cache_on method returning nil or false still contributes that value to the key rather than disabling caching.
A component that declares cache_on can’t accept content from its callers. A block, with_content, or a slot set by the caller isn’t part of the cache key, so passing one raises ContentPassedToCachedComponentError:
<%# Raises: the block's content isn't in the cache key %>
<%= render PostComponent.new(post: @post) do %>
Hello
<% end %>
The error is raised even when caching is disabled, so the conflict surfaces in development and test rather than only in production. See Caveats for how to restructure a component that needs to take content.
cache_on is inherited, so declaring it on a base class opts every subclass into self-caching, and into that restriction. Declare it on the components that should cache themselves rather than on ApplicationComponent.
Reading a component’s digest
.cache_digest returns the digest of everything the component renders from. It works outside a request, where no view context exists:
PostComponent.cache_digest # => "a1b2c3..."
Use it when a cache needs to be tied to a component’s source but is written somewhere the component isn’t rendered, such as a background job:
Rails.cache.fetch(["post-summary", post, PostComponent.cache_digest]) do
expensive_summary_for(post)
end
Declaring dependencies static analysis can’t see
Dependencies are discovered by scanning template and Ruby source for literal references, so renders resolved at runtime are invisible:
<%= render @component %>
def call
render "posts/#{@post.style}" # interpolated, so not tracked
end
Declare these with Rails’ # Template Dependency: comment, in either the Ruby file or the template. Partials are named by path:
class PostComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable
# Template Dependency: posts/byline
end
Components are named by class, listing each one the component might render:
class PostComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable
# Template Dependency: PostSummaryComponent
# Template Dependency: PostDetailComponent
def call
render(@detailed ? PostDetailComponent : PostSummaryComponent).new(post: @post)
end
end
The same works in a template, where the branch is often the more natural place for it:
<% if params[:style] == "summary" %>
<%# Template Dependency: PostSummaryComponent %>
<% component = PostSummaryComponent %>
<% else %>
<%# Template Dependency: PostDetailComponent %>
<% component = PostDetailComponent %>
<% end %>
<%= render component.new(post: @post) %>
Declared components must include ViewComponent::ExperimentallyCacheable themselves, since a component that hasn’t opted in has no digest to depend on.
Caveats
Self-caching components can’t take content from their callers. Besides a block, this covers with_content and slots set by the caller:
<%# Also raises ContentPassedToCachedComponentError %>
<%= render PostComponent.new(post: @post) do |component| %>
<% component.with_header { "Hello" } %>
<% end %>
Slots a component fills in for itself with a default_* method are part of its own output, not the caller’s, so those are cached normally:
class PostComponent < ViewComponent::Base
include ViewComponent::ExperimentallyCacheable
renders_one :header
cache_on :post
def default_header
post.title # cached, because the component decides it
end
end
To cache a component that takes content, move the content into the component and derive it from values declared in cache_on. Components that don’t declare cache_on are unaffected: they still accept content and slots, and a <% cache %> block around them still invalidates correctly.
cache_on methods run before the component renders, so they can only depend on the component’s own state, not on helpers or the view context. A cache key that depends on the view context is usually a sign the value should be passed to the component instead.
Included modules aren’t tracked. A component’s superclasses are, but a module included into a component isn’t, since a module has no template or source file of its own to hash. Use # Template Dependency: for those.