You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add declarative cache_tags revalidation to Pro fragment caching (revalidateTag analog) (#3964)
## Summary
Implements the maintainer-signed-off RFC on #3871 (v1 scope; SWR
explicitly deferred per the 2026-06-11 triage).
**What:** `cache_tags:` on all four Pro `cached_*` helpers; a
`Rails.cache`-backed tag→key index; `ReactOnRailsPro.revalidate_tag` /
`revalidate_tags`; `ReactOnRailsPro::Cache::Revalidates` AR concern
(`revalidates_react_cache`, `after_commit`); config
`cache_tag_index_expires_in` (7d) / `cache_tag_index_max_keys` (5,000);
docs (caching guide section with Next.js mapping, Pro fragment-caching
section) and changelog. `cache_key:` semantics unchanged; `cache_tags:`
is purely additive. Zero OSS package code changes.
**Contract (per RFC Option A):** index appends are lossy-OK
read-modify-write; tag revalidation is best-effort, correctness bounded
by `expires_in` (dev warning without an expiry); missing/evicted index →
no-op returning 0. A Redis-native-sets fast path remains a welcome
follow-up optimization, never a correctness requirement.
## Codex Decision Log
- **Non-blocking:** index payload format -> **Decision:** `{keys:,
expires_at:}` (not bare array) to implement the RFC's max-TTL merge
portably; `v1` key segment covers format evolution.
- **Non-blocking:** which key form to record -> **Decision:** the
store's logical name via private
`expanded_key`/`namespace_key`/`merged_options` (public
`expand_cache_key` prefers `cache_key_with_version` + `RAILS_CACHE_ID`
and would miss entries). FileStore encoding covered by spec.
- **Non-blocking:** AR tag derivation -> **Decision:**
`model_name.cache_key/id` -- equal to version-less `cache_key`, stable
even with `cache_versioning = false`.
- **Non-blocking:** tag validation timing -> **Decision:** at
registration (miss path only), so cache hits never pay
Proc/normalization cost.
- **Non-blocking:** mutable custom tags (stale grouping after e.g.
author change) -> **Decision:** documented as bounded-by-`expires_in`
with a `previous_changes` recipe, not implemented -- per
descoped-v1/no-over-engineering triage note.
- **Non-blocking:** changelog location -> **Decision:** unified
`/CHANGELOG.md` (`react_on_rails_pro/CHANGELOG.md` is a deprecation
stub).
- **Non-blocking:** `llms-full.txt` regeneration necessarily absorbs
main's pre-existing staleness that open PR #3958 also fixes --
regenerate on rebase if #3958 lands first.
- **DISCUSS resolved:** delete ordering in `revalidate_tag` ->
**Decision:** keep clearing the tag index before deleting entries.
Reversing the order improves crash retryability for old entries, but it
can delete fresh re-registrations discovered through a stale index
snapshot. The retained order protects fresh content;
crash-after-index-clear orphaning remains bounded by tagged entry expiry
under the RFC's best-effort contract.
- **Compatibility:** tag index key shape -> **Decision:** index keys are
SHA-256 digests under `rorp:tag:v1:` so valid-but-long or
control-character tag names do not become invalid Memcached-style cache
keys.
- **Expiry semantics:** `expires_at` handling -> **Decision:** when
Rails cache entries honor `expires_at`, it takes precedence over
`expires_in`; older Rails strip unsupported `expires_at` and use the TTL
the entry store will actually honor. Expired absolute times clamp to
immediate expiry instead of propagating negative TTLs.
- **Streaming cache writes:** tag-index options are computed before
stream completion so the index TTL is not shortened by render duration;
entry write options are recomputed at completion so old-Rails absolute
expiries still expire at the intended wall-clock time.
## Validation
- `bundle exec rspec spec/react_on_rails_pro/cache
spec/react_on_rails_pro/cache_spec.rb` → `38 examples, 0 failures`
- Full Pro gem suite `bundle exec rspec spec/react_on_rails_pro` → `545
examples, 3 failures, 1 pending`; the 3 failures
(`prod_binstub_spec.rb`) pre-exist on clean origin/main (verified via
stashed run: `3 examples, 3 failures`) — environment-related, untouched
here
- Pro dummy app (built `build:test` assets + node renderer on :3800):
`bundle exec rspec spec/helpers/react_on_rails_pro_helper_spec.rb
spec/models/cache_revalidates_spec.rb` → `64 examples, 0 failures` —
includes the cached round-trip, stream HIT→revalidate→MISS, and async
HIT→revalidate→MISS tag-busting proofs
- `bundle exec rubocop --ignore-parent-exclusion` (Pro) → `235 files
inspected, no offenses detected`; OSS rubocop → `218 files, no offenses`
- `bundle exec rake rbs:validate` → `✓ RBS validation passed`
- `node script/generate-llms-full.mjs --check` → `✓ llms-full.txt is
current`
- `script/check-docs-sidebar` → pass; `pnpm start format.listDifferent`
→ pass
## Review gate
`codex review --base origin/main` run to completion 4 times; all
findings fixed and re-reviewed (namespace-aware deletes, `expires_at`
index TTL, stable AR identity under `cache_versioning = false`, logical
key recording + FileStore spec + `delete_multi` fallback). A 5th
confirmation pass timed out twice at the 580s tool cap with no findings
emitted; recorded as incomplete rather than looping further.
Closes#3871
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches production fragment-cache read/write and invalidation across
sync, stream, and async paths; behavior is best-effort and TTL-bounded,
with broad test coverage but reliance on shared cache stores and custom
stores honoring `namespace: nil` on deletes.
>
> **Overview**
> **[Pro]** Adds **tag-based cache revalidation** (Next.js
`revalidateTag`-style) on top of existing fragment caching: all four
`cached_*` helpers accept optional `cache_tags:`, misses register keys
in a `Rails.cache` tag→key index, and **`ReactOnRailsPro.revalidate_tag`
/ `revalidate_tags`** delete indexed entries.
**`ReactOnRailsPro::Cache::Revalidates`** (`revalidates_react_cache`,
`after_commit`) wires invalidation from ActiveRecord;
**`cache_tag_index_expires_in`** and **`cache_tag_index_max_keys`**
bound index growth. Helpers also normalize **`expires_at`** for writes
and skip caching when expiry has already passed.
>
> **`cache_key:` behavior is unchanged**; tagging is additive and
documented as **best-effort**, with correctness bounded by
**`expires_in`**.
>
> Docs/changelog/llms outputs describe the contract and Next.js mapping;
**`docs/.llms-exclusions`** trims some pages to stay under the llms cap;
**`.lychee.toml`** ignores `vite.dev` in CI link checks.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
14ead8d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Tag-based cache revalidation for cached React fragments via new
`revalidate_tag` / `revalidate_tags` APIs, plus model-level after-commit
invalidation.
* Added cache tag indexing with configurable TTL and maximum keys; warns
in development when tags are used without expiry.
* `cache_tags` are recorded only when a cache miss writes the entry.
* **Documentation**
* Expanded guides and examples for tag normalization/validation,
operational semantics, configuration knobs, and Next.js mapping; updated
changelog.
* **Tests**
* Added coverage for tag normalization, registration timing,
revalidation behavior (including async/stream), and model-driven
invalidation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Merge Readiness Criteria
Current evaluation as of 2026-06-15 for head
`14ead8daca9e9a73044f66b800594447f9cb30bf`.
- **Release mode:** `accelerated-rc`, from canonical release gate #3823
(`Mode: accelerated-rc`).
- **Current head SHA:** `14ead8daca9e9a73044f66b800594447f9cb30bf`.
- **CI/check status:** Complete for current head: 33 passing checks and
4 expected/path-selected skips; no pending or failing checks. Passing
checks include `check-llms-full`, Pro node renderer E2E/RSpec, Pro lint,
package JS tests, examples, CodeQL, `claude-review`, Cursor Bugbot,
CodeRabbit, and benchmark suites (`Core benchmarks`, `Pro benchmarks`
shards 1/2 and 2/2, `Pro Node Renderer benchmarks`). `mergeStateStatus`
is `CLEAN`.
- **Review-thread status:** Paginated GraphQL review-thread sweep
reports 85 total threads and `unresolved=0` immediately before merge.
- **Review feedback triage:** Requested-change/review feedback was fixed
or explicitly triaged in the PR history. The latest branch update
resolved generated `llms-full.txt` drift against current `main`; no
#3963-reserved docs files were touched.
- **Validation run:** Current-head GitHub checks above are the merge
gate. Local/worker validation for the merge resolution reported `git
diff --check`, `node script/generate-llms-full.mjs --check`, `pnpm start
format.listDifferent`, OSS/Pro RuboCop, focused Pro cache specs, dummy
cache revalidation spec, and helper spec after starting the node
renderer all passing.
- **Label/CI decision:** Labels: `full-ci`, `benchmark`. This PR touches
Pro fragment caching/cache tag revalidation and performance-sensitive
paths, so both labels are appropriate; benchmark checks completed
successfully.
- **Known residual risk:** Cache invalidation behavior is
release-relevant and performance-sensitive, but current-head full CI,
benchmarks, and review-thread gates are clean. No unresolved blocker
remains.
- **Merge recommendation:** Merge now. Maintainer marked #3964 approved
in the batch instruction; current-head gates satisfy AGENTS.md merge
qualification.
## Agent Merge Confidence
Mode: accelerated-rc
Current head SHA: `14ead8daca9e9a73044f66b800594447f9cb30bf`
Score: 8/10
Auto-merge recommendation: yes, under explicit maintainer approval in
the batch thread.
Affected areas: Pro fragment caching, cache tag revalidation, generated
docs/LLMS, benchmarks.
CI detector: full CI + benchmark labels; current-head full CI and
benchmarks completed.
Validation run:
- Current-head GitHub checks -> 33 pass / 4 expected skips / 0 pending /
0 failed.
- Local/worker merge-update validation -> LLMS, format, whitespace,
RuboCop, focused cache specs, dummy cache revalidation spec, and helper
spec passed.
Review/check gate:
- GitHub checks: complete for
`14ead8daca9e9a73044f66b800594447f9cb30bf`; skips are path-selected
helper skips.
- Review threads: GraphQL unresolved count is 0.
- Current-head reviewer verdicts: `claude-review`, Cursor Bugbot,
CodeRabbit completed with no unresolved blocker threads.
Known residual risk: release-relevant cache invalidation change,
mitigated by full CI, focused validation, and benchmark coverage.
Finalized by: maintainer approval in the Codex batch thread (`#3964`
marked “Approved by maintainer”).
Confidence note:
- Validated: current-head full CI/benchmarks plus local/worker focused
validation above.
- Evidence: `gh pr checks 3964` -> 33 pass / 4 skip / 0 pending / 0
failed; GraphQL review threads -> 85 total / 0 unresolved;
`mergeStateStatus` -> `CLEAN`.
- UNKNOWN: none that affects merge qualification.
- Residual risk: performance/cache behavior remains the main
release-watch surface.
- Decision points: 1 (maintainer approval/merge authorization).
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,6 +26,7 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
26
26
27
27
#### Added
28
28
29
+
- **[Pro]** **Tag-based cache revalidation (a Next.js `revalidateTag` analog)**: The fragment-caching helpers (`cached_react_component`, `cached_react_component_hash`, `cached_stream_react_component`, `cached_async_react_component`) now accept an optional `cache_tags:` option (String, Proc, any object responding to `cache_key` such as an ActiveRecord model, or an Array of any mix), and the new `ReactOnRailsPro.revalidate_tag(tag)` / `revalidate_tags(*tags)` API deletes every cached entry registered under a tag via a `Rails.cache`-backed tag->key index. A new `ReactOnRailsPro::Cache::Revalidates` ActiveRecord concern (`revalidates_react_cache`) drives revalidation from `after_commit`, so the model that owns the data also owns cache invalidation (and composes with `touch:`). Revalidation is best-effort with correctness bounded by `expires_in` (a development-mode warning fires when `cache_tags:` is used without it); index growth is bounded by the new `config.cache_tag_index_expires_in` (default 7 days) and `config.cache_tag_index_max_keys` (default 5,000) settings. Existing `cache_key:`-only behavior is unchanged. Closes [Issue 3871](https://github.com/shakacode/react_on_rails/issues/3871). [PR 3964](https://github.com/shakacode/react_on_rails/pull/3964) by [justin808](https://github.com/justin808).
29
30
- **React 19 root error callbacks**: `ReactOnRails.setOptions({ rootErrorHandlers: { onRecoverableError, onCaughtError, onUncaughtError } })` registers React's root error callbacks globally; React on Rails applies them to every `hydrateRoot`/`createRoot` call it makes and invokes them with an extra context argument whose `componentName` and `domNodeId` fields are optional. In development, recoverable hydration errors now log an actionable React on Rails message (component name, dom id, component stack, and a link to the new [Debugging Hydration Mismatches guide](https://reactonrails.com/docs/building-features/debugging-hydration-mismatches)) alongside React's default error reporting, which stays intact so window-'error'-based tooling keeps working. Partial `rootErrorHandlers` updates merge per key, so registering one callback later does not drop the others. On React <19 (and <18 for `onRecoverableError`), React on Rails retains registrations for future upgrades, but the current runtime cannot invoke unsupported callbacks and logs a one-time console warning. On React on Rails Pro RSC/streaming hydration paths, user callbacks chain with (never replace) Pro's internal recoverable-error handler. Addresses [Issue 3892](https://github.com/shakacode/react_on_rails/issues/3892). [PR 3933](https://github.com/shakacode/react_on_rails/pull/3933) by [justin808](https://github.com/justin808).
Copy file name to clipboardExpand all lines: docs/oss/building-features/caching.md
+90Lines changed: 90 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -211,6 +211,96 @@ case you have, React on Rails handles it.
211
211
212
212
---
213
213
214
+
## Tag-Based Revalidation
215
+
216
+
Cache keys answer "is this entry still current?" at read time. Tags answer a different question: "delete everything that depends on this data, right now." With `cache_tags:`, you attach declarative invalidation handles to fragment-cached components and bust them all with one call — the React on Rails Pro analog of Next.js `revalidateTag`.
217
+
218
+
```erb
219
+
<%= cached_react_component("PostShow",
220
+
cache_key: [@post, I18n.locale],
221
+
cache_tags: [@post, @post.author],
222
+
cache_options: { expires_in: 12.hours }) do
223
+
{ post: @post.to_props }
224
+
end %>
225
+
```
226
+
227
+
```ruby
228
+
# Anywhere in Ruby — controller, job, service object, console:
229
+
ReactOnRailsPro.revalidate_tag(post) # => number of cache entries deleted
`cache_tags:` is accepted by all four cached helpers — `cached_react_component`, `cached_react_component_hash`, `cached_stream_react_component`, and `cached_async_react_component` — and is purely additive: `cache_key:` semantics are unchanged, and `cache_key:` is still required.
234
+
235
+
### Tag forms and normalization
236
+
237
+
A tag can be:
238
+
239
+
- a **String** — passed through unchanged (`"post:42"`)
240
+
- a **Symbol or Numeric** — stringified via `.to_s` (`:featured` -> `"featured"`, `42` -> `"42"`)
241
+
- an object responding to **`#cache_key`** (any ActiveRecord model) — ActiveRecord-style records normalize to the stable identity `posts/42` (equal to the version-less `record.cache_key`, and stable even when `cache_versioning` is disabled), so the tag stays valid as the record changes; other objects pass their `#cache_key` through. Objects with both `model_name` and `id` always resolve to `collection/id`; pass an explicit String tag if you want a different key.
242
+
- a **Proc** (arity 0) returning any accepted form
243
+
- an **Array** of any mix of the above
244
+
245
+
Tags that normalize to blank raise `ReactOnRailsPro::Error`. Tag coarsely (`post:42`, `tenant:7`, `posts:index`) — never per-user or per-request; see the index bounds below.
246
+
247
+
### Revalidating from the model layer
248
+
249
+
Include the `Revalidates` concern so the model that owns the data also owns cache invalidation. It runs in `after_commit`, so it never fires for a rolled-back transaction and fires only after the new data is visible to the re-rendering request:
250
+
251
+
```ruby
252
+
classPost < ApplicationRecord
253
+
includeReactOnRailsPro::Cache::Revalidates
254
+
255
+
revalidates_react_cache # default tag: record.cache_key, e.g. "posts/42"
This covers create/update/destroy and `touch` — so `belongs_to ..., touch: true` composes for free: touching the parent fires the parent's revalidation. The standard Rails callback caveat applies: `update_column`, `update_all`, `delete_all`, and other callback-skipping writes do not trigger revalidation; call `ReactOnRailsPro.revalidate_tags(record)` yourself after such writes.
262
+
263
+
One more caveat for custom tag blocks: the block runs in `after_commit` and sees only the record's **new** values. If a custom tag derives from a mutable attribute (e.g. `"author:#{post.author_id}"` and a post moves to a different author), the old grouping's entries are not revalidated — they expire via `expires_in`. Prefer tags derived from the record's own identity, or revalidate the old grouping explicitly (`previous_changes` in `after_commit` has the prior value).
264
+
265
+
### How it works, and the contract
266
+
267
+
On every tagged cache write, the final cache key is appended to a per-tag index entry in `Rails.cache` (keyed by a SHA-256 digest under `rorp:tag:v1:` so long tag names and whitespace do not violate cache-store key limits). `revalidate_tag` reads the index, deletes the recorded entries with `delete_multi`, then deletes the index entry. A missing index (never-written tag, evicted entry, `:null_store`) means "nothing to revalidate" — never an error.
268
+
269
+
**Tag revalidation is best-effort; correctness is bounded by `expires_in`.**`ActiveSupport::Cache` has no atomic set-append, so the index append is a read-modify-write: two processes caching different entries under the same tag at the same moment can race, and one entry can be lost _from the index_ (the cached data itself is never lost). A lost index entry simply survives `revalidate_tag` and expires via its own `expires_in`. The same applies when the index entry itself is LRU-evicted. Therefore:
270
+
271
+
-**Always set `expires_in` (or `expires_at`) on tagged entries.** It is the upper bound on how long a missed invalidation can serve stale HTML. In development, React on Rails Pro logs a warning when `cache_tags:` is used without an expiry.
272
+
-**Use a shared cache store in production** — Redis or Memcached. With `:memory_store` the index is per-process, so `revalidate_tag` in one process cannot see entries written by another; with `:null_store` tags are inert.
273
+
-**Keep cache deletion failures bounded by expiry.**`revalidate_tag` clears the tag index before deleting the indexed entries to reduce re-registration races. If the cache store raises during deletion, any surviving entries are orphaned from that tag and only expire via their own `expires_in`.
274
+
-**Custom cache stores must honor `namespace: nil` in `delete_multi` and `delete`.** The tag index records the fully namespaced logical keys that Rails wrote, then suppresses the store default namespace at delete time. Stores that ignore `options[:namespace]` can silently miss tag-revalidation deletes.
275
+
276
+
Two config knobs bound the index (defaults shown):
277
+
278
+
```ruby
279
+
ReactOnRailsPro.configure do |config|
280
+
config.cache_tag_index_expires_in =7.days # index TTL ceiling for entries without expires_in
281
+
config.cache_tag_index_max_keys =5_000# keys recorded per tag; oldest dropped beyond this
282
+
end
283
+
```
284
+
285
+
When a tagged entry has `expires_in`, the index entry's TTL automatically covers it (plus slack). When a tag exceeds the per-tag key cap, the oldest keys are dropped with a logged warning — those entries fall back to plain TTL expiration.
286
+
287
+
Note that tags solve **data**-driven invalidation only. Deploy invalidation is already handled by the server-bundle digest in the cache key (see [Cache Warming](#cache-warming)) — a deploy cold-starts prerendered fragment caches regardless of tags.
288
+
289
+
### Next.js mapping
290
+
291
+
| Next.js 16 (Cache Components) | React on Rails Pro |
|`revalidateTag(tag, 'max')` / SWR profiles | Not yet supported (stale-while-revalidate is a possible future addition) |
299
+
300
+
Like Next.js's default `revalidateTag`, revalidation deletes: the next request re-renders and re-registers. There is no background refresh.
301
+
302
+
---
303
+
214
304
## Cache Warming
215
305
216
306
Fragment cache keys include the server bundle digest, which means every deploy creates new cache keys. This is correct — rendered output must match the current bundle — but it means every deploy starts with a cold cache. Under live traffic, this creates a synchronized storm of cache misses: every user request triggers full SSR, database queries for props assembly, and JS evaluation simultaneously.
Copy file name to clipboardExpand all lines: docs/pro/fragment-caching.md
+31Lines changed: 31 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -41,6 +41,37 @@ end %>
41
41
42
42
A `cached_react_component_hash` variant is also available for cases where you need to extract metadata (like `<title>`) from the rendered output.
43
43
44
+
## Tag-Based Revalidation
45
+
46
+
Cache keys handle "is this entry still current?" at read time. For the write side — "this record changed, bust every cached component that depends on it" — tag the entries and revalidate by tag (the React on Rails Pro analog of Next.js `revalidateTag`):
47
+
48
+
```erb
49
+
<%= cached_react_component("PostShow",
50
+
cache_key: [@post, I18n.locale],
51
+
cache_tags: [@post],
52
+
cache_options: { expires_in: 12.hours }) do
53
+
{ post: @post.to_props }
54
+
end %>
55
+
```
56
+
57
+
```ruby
58
+
ReactOnRailsPro.revalidate_tag(post) # deletes every entry tagged with post.cache_key
59
+
```
60
+
61
+
Or let the model own its invalidation via `after_commit`:
62
+
63
+
```ruby
64
+
classPost < ApplicationRecord
65
+
includeReactOnRailsPro::Cache::Revalidates
66
+
67
+
revalidates_react_cache # default tag: record.cache_key, e.g. "posts/42"
68
+
end
69
+
```
70
+
71
+
Tag revalidation is best-effort and bounded by `expires_in` — always set it on tagged entries, and use a shared cache store (Redis/Memcached) in production. If a cache store raises while deleting tagged entries, the tag index may already be cleared; any surviving entries can no longer be found by that tag and will only drain through their own expiry. See the [Tag-Based Revalidation section](../oss/building-features/caching.md#tag-based-revalidation) of the caching guide for the full contract, tag normalization rules, index configuration, and the Next.js `revalidateTag` mapping.
72
+
73
+
ActiveRecord-style tag objects normalize to `collection/id` (for example `posts/42`) before they are indexed. Pass an explicit String tag if a value object exposes `model_name` and `id` but should use a custom key.
74
+
44
75
## Cache Warming
45
76
46
77
Every deploy creates new cache keys for prerendered components (because the server bundle digest is included in the cache key when `prerender: true`). For client-only cached components, version your own cache key to invalidate on deploy. To avoid a storm of cold-cache misses under live traffic, warm your highest-traffic pages in background jobs immediately after deploy.
Copy file name to clipboardExpand all lines: llms-full-pro.txt
+31Lines changed: 31 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -3999,6 +3999,37 @@ end %>
3999
3999
4000
4000
A `cached_react_component_hash` variant is also available for cases where you need to extract metadata (like `<title>`) from the rendered output.
4001
4001
4002
+
## Tag-Based Revalidation
4003
+
4004
+
Cache keys handle "is this entry still current?" at read time. For the write side — "this record changed, bust every cached component that depends on it" — tag the entries and revalidate by tag (the React on Rails Pro analog of Next.js `revalidateTag`):
4005
+
4006
+
```erb
4007
+
<%= cached_react_component("PostShow",
4008
+
cache_key: [@post, I18n.locale],
4009
+
cache_tags: [@post],
4010
+
cache_options: { expires_in: 12.hours }) do
4011
+
{ post: @post.to_props }
4012
+
end %>
4013
+
```
4014
+
4015
+
```ruby
4016
+
ReactOnRailsPro.revalidate_tag(post) # deletes every entry tagged with post.cache_key
4017
+
```
4018
+
4019
+
Or let the model own its invalidation via `after_commit`:
4020
+
4021
+
```ruby
4022
+
class Post < ApplicationRecord
4023
+
include ReactOnRailsPro::Cache::Revalidates
4024
+
4025
+
revalidates_react_cache # default tag: record.cache_key, e.g. "posts/42"
4026
+
end
4027
+
```
4028
+
4029
+
Tag revalidation is best-effort and bounded by `expires_in` — always set it on tagged entries, and use a shared cache store (Redis/Memcached) in production. If a cache store raises while deleting tagged entries, the tag index may already be cleared; any surviving entries can no longer be found by that tag and will only drain through their own expiry. See the [Tag-Based Revalidation section](../oss/building-features/caching.md#tag-based-revalidation) of the caching guide for the full contract, tag normalization rules, index configuration, and the Next.js `revalidateTag` mapping.
4030
+
4031
+
ActiveRecord-style tag objects normalize to `collection/id` (for example `posts/42`) before they are indexed. Pass an explicit String tag if a value object exposes `model_name` and `id` but should use a custom key.
4032
+
4002
4033
## Cache Warming
4003
4034
4004
4035
Every deploy creates new cache keys for prerendered components (because the server bundle digest is included in the cache key when `prerender: true`). For client-only cached components, version your own cache key to invalidate on deploy. To avoid a storm of cold-cache misses under live traffic, warm your highest-traffic pages in background jobs immediately after deploy.
0 commit comments