|
| 1 | +--- |
| 2 | +layout: default |
| 3 | +title: Caching |
| 4 | +parent: How-to guide |
| 5 | +--- |
| 6 | + |
| 7 | +# Caching |
| 8 | + |
| 9 | +Experimental |
| 10 | +{: .label .label-yellow } |
| 11 | + |
| 12 | +Since 4.14.0 |
| 13 | +{: .label } |
| 14 | + |
| 15 | +**This API is experimental.** It may change or be removed in a non-major release. Please share feedback in [#234](https://github.com/ViewComponent/view_component/issues/234). |
| 16 | + |
| 17 | +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. |
| 18 | + |
| 19 | +Components are invisible to that mechanism. |
| 20 | + |
| 21 | +```erb |
| 22 | +<% cache @post do %> |
| 23 | + <%= render PostComponent.new(post: @post) %> |
| 24 | +<% end %> |
| 25 | +``` |
| 26 | + |
| 27 | +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. |
| 28 | + |
| 29 | +## Opting in |
| 30 | + |
| 31 | +Include `ViewComponent::ExperimentallyCacheable` in each component that should participate in caching: |
| 32 | + |
| 33 | +```ruby |
| 34 | +class PostComponent < ViewComponent::Base |
| 35 | + include ViewComponent::ExperimentallyCacheable |
| 36 | + |
| 37 | + def initialize(post:) |
| 38 | + @post = post |
| 39 | + end |
| 40 | +end |
| 41 | +``` |
| 42 | + |
| 43 | +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. |
| 44 | + |
| 45 | +## Self-caching |
| 46 | + |
| 47 | +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. |
| 48 | + |
| 49 | +```ruby |
| 50 | +class PostComponent < ViewComponent::Base |
| 51 | + include ViewComponent::ExperimentallyCacheable |
| 52 | + |
| 53 | + cache_on :post |
| 54 | + |
| 55 | + def initialize(post:) |
| 56 | + @post = post |
| 57 | + end |
| 58 | + |
| 59 | + private |
| 60 | + |
| 61 | + attr_reader :post |
| 62 | +end |
| 63 | +``` |
| 64 | + |
| 65 | +Every call site is now cached automatically: |
| 66 | + |
| 67 | +```erb |
| 68 | +<%= render PostComponent.new(post: @post) %> |
| 69 | +``` |
| 70 | + |
| 71 | +Which is equivalent to writing: |
| 72 | + |
| 73 | +```erb |
| 74 | +<% cache [@post, PostComponent.cache_digest] do %> |
| 75 | + <%= render PostComponent.new(post: @post) %> |
| 76 | +<% end %> |
| 77 | +``` |
| 78 | + |
| 79 | +The cache key combines: |
| 80 | + |
| 81 | +- the component's virtual path |
| 82 | +- its digest, computed by Rails' `ActionView::Digestor` |
| 83 | +- the requested format and variant |
| 84 | +- the current `I18n.locale` |
| 85 | +- the values returned by the `cache_on` methods |
| 86 | + |
| 87 | +Caching is skipped unless `perform_caching` is enabled on the controller, matching the behavior of Rails' `cache` helper. |
| 88 | + |
| 89 | +To cache only some renders, pass `if:` or `unless:`. Both accept a method name or a proc evaluated on the component: |
| 90 | + |
| 91 | +```ruby |
| 92 | +class PostComponent < ViewComponent::Base |
| 93 | + include ViewComponent::ExperimentallyCacheable |
| 94 | + |
| 95 | + cache_on :post, unless: -> { post.draft? } |
| 96 | + |
| 97 | + def initialize(post:) |
| 98 | + @post = post |
| 99 | + end |
| 100 | + |
| 101 | + private |
| 102 | + |
| 103 | + attr_reader :post |
| 104 | +end |
| 105 | +``` |
| 106 | + |
| 107 | +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. |
| 108 | + |
| 109 | +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`: |
| 110 | + |
| 111 | +```erb |
| 112 | +<%# Raises: the block's content isn't in the cache key %> |
| 113 | +<%= render PostComponent.new(post: @post) do %> |
| 114 | + Hello |
| 115 | +<% end %> |
| 116 | +``` |
| 117 | + |
| 118 | +The error is raised even when caching is disabled, so the conflict surfaces in development and test rather than only in production. See [Caveats](#caveats) for how to restructure a component that needs to take content. |
| 119 | + |
| 120 | +`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`. |
| 121 | + |
| 122 | +## Reading a component's digest |
| 123 | + |
| 124 | +`.cache_digest` returns the digest of everything the component renders from. It works outside a request, where no view context exists: |
| 125 | + |
| 126 | +```ruby |
| 127 | +PostComponent.cache_digest # => "a1b2c3..." |
| 128 | +``` |
| 129 | + |
| 130 | +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: |
| 131 | + |
| 132 | +```ruby |
| 133 | +Rails.cache.fetch(["post-summary", post, PostComponent.cache_digest]) do |
| 134 | + expensive_summary_for(post) |
| 135 | +end |
| 136 | +``` |
| 137 | + |
| 138 | +## Declaring dependencies static analysis can't see |
| 139 | + |
| 140 | +Dependencies are discovered by scanning template and Ruby source for literal references, so renders resolved at runtime are invisible: |
| 141 | + |
| 142 | +```erb |
| 143 | +<%= render @component %> |
| 144 | +``` |
| 145 | + |
| 146 | +```ruby |
| 147 | +def call |
| 148 | + render "posts/#{@post.style}" # interpolated, so not tracked |
| 149 | +end |
| 150 | +``` |
| 151 | + |
| 152 | +Declare these with Rails' `# Template Dependency:` comment, in either the Ruby file or the template. Partials are named by path: |
| 153 | + |
| 154 | +```ruby |
| 155 | +class PostComponent < ViewComponent::Base |
| 156 | + include ViewComponent::ExperimentallyCacheable |
| 157 | + |
| 158 | + # Template Dependency: posts/byline |
| 159 | +end |
| 160 | +``` |
| 161 | + |
| 162 | +Components are named by class, listing each one the component might render: |
| 163 | + |
| 164 | +```ruby |
| 165 | +class PostComponent < ViewComponent::Base |
| 166 | + include ViewComponent::ExperimentallyCacheable |
| 167 | + |
| 168 | + # Template Dependency: PostSummaryComponent |
| 169 | + # Template Dependency: PostDetailComponent |
| 170 | + |
| 171 | + def call |
| 172 | + render(@detailed ? PostDetailComponent : PostSummaryComponent).new(post: @post) |
| 173 | + end |
| 174 | +end |
| 175 | +``` |
| 176 | + |
| 177 | +The same works in a template, where the branch is often the more natural place for it: |
| 178 | + |
| 179 | +```erb |
| 180 | +<% if params[:style] == "summary" %> |
| 181 | + <%# Template Dependency: PostSummaryComponent %> |
| 182 | + <% component = PostSummaryComponent %> |
| 183 | +<% else %> |
| 184 | + <%# Template Dependency: PostDetailComponent %> |
| 185 | + <% component = PostDetailComponent %> |
| 186 | +<% end %> |
| 187 | +<%= render component.new(post: @post) %> |
| 188 | +``` |
| 189 | + |
| 190 | +Declared components must include `ViewComponent::ExperimentallyCacheable` themselves, since a component that hasn't opted in has no digest to depend on. |
| 191 | + |
| 192 | +## Caveats |
| 193 | + |
| 194 | +**Self-caching components can't take content from their callers.** Besides a block, this covers `with_content` and slots set by the caller: |
| 195 | + |
| 196 | +```erb |
| 197 | +<%# Also raises ContentPassedToCachedComponentError %> |
| 198 | +<%= render PostComponent.new(post: @post) do |component| %> |
| 199 | + <% component.with_header { "Hello" } %> |
| 200 | +<% end %> |
| 201 | +``` |
| 202 | + |
| 203 | +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: |
| 204 | + |
| 205 | +```ruby |
| 206 | +class PostComponent < ViewComponent::Base |
| 207 | + include ViewComponent::ExperimentallyCacheable |
| 208 | + |
| 209 | + renders_one :header |
| 210 | + |
| 211 | + cache_on :post |
| 212 | + |
| 213 | + def default_header |
| 214 | + post.title # cached, because the component decides it |
| 215 | + end |
| 216 | +end |
| 217 | +``` |
| 218 | + |
| 219 | +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. |
| 220 | + |
| 221 | +**`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. |
| 222 | + |
| 223 | +**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. |
0 commit comments