Skip to content

Commit 848e23b

Browse files
justin808claude
andauthored
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>
1 parent 7ee628c commit 848e23b

21 files changed

Lines changed: 2345 additions & 492 deletions

.lychee.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ exclude = [
135135
# ============================================================================
136136
# EXTERNAL DOC PAGES WITH CI CONNECTIVITY FAILURES
137137
# ============================================================================
138+
'^https://vite\.dev(/.*)?$', # Connection failed from CI
138139
'^https://tanstack\.com/query/latest/docs/framework/react/guides/ssr$', # Connection failed from CI
139140

140141
# ============================================================================

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
2626

2727
#### Added
2828

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).
2930
- **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).
3031

3132
#### Changed

docs/.llms-exclusions

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
# Docs intentionally excluded from the generated llms-full.txt
2-
# (redirect stubs with no real content). One doc ID per line.
1+
# Docs intentionally excluded from the generated llms-full.txt.
2+
# Prefer redirect stubs with no real content. Full docs may be temporarily
3+
# excluded only to stay below the hard split threshold until llms-full is split.
4+
# One doc ID per line.
35
# Inline comments (after #) are allowed.
46
# Used by script/generate-llms-full.mjs.
57

@@ -11,3 +13,16 @@ misc/asset-pipeline
1113

1214
# URL-compatibility stub that redirects to pro/react-on-rails-pro
1315
pro/home-pro
16+
17+
# Contributing/Resources pages (linked from introduction.md, not the sidebar).
18+
# Excluded until llms-full is split so product/API docs remain under the cap.
19+
misc/doctrine
20+
misc/style
21+
misc/updating-dependencies
22+
misc/credits
23+
misc/articles
24+
misc/tips
25+
26+
# Older release-note detail pages remain linked from the release-notes index.
27+
# Excluded until llms-full is split so current product/API docs stay under the cap.
28+
upgrading/release-notes/16.1.0

docs/oss/building-features/caching.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,96 @@ case you have, React on Rails handles it.
211211

212212
---
213213

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
230+
ReactOnRailsPro.revalidate_tags(post, post.author)
231+
```
232+
233+
`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+
class Post < ApplicationRecord
253+
include ReactOnRailsPro::Cache::Revalidates
254+
255+
revalidates_react_cache # default tag: record.cache_key, e.g. "posts/42"
256+
# or custom / additional tags:
257+
# revalidates_react_cache { |post| ["post:#{post.id}", "author:#{post.author_id}"] }
258+
end
259+
```
260+
261+
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 |
292+
| --------------------------------------------------- | --------------------------------------------------------------------------------------- |
293+
| `'use cache'` on a function/component | `cached_react_component` / `cached_react_component_hash` / streaming and async variants |
294+
| `cacheTag('post-42')` inside the cached scope | `cache_tags: ["post:42"]` helper option |
295+
| `revalidateTag('post-42')` in a Server Action | `ReactOnRailsPro.revalidate_tag("post:42")` — anywhere in Ruby, incl. `after_commit` |
296+
| Manually wiring `revalidateTag` into every mutation | `include ReactOnRailsPro::Cache::Revalidates` — invalidation rides the AR transaction |
297+
| `cacheLife` profiles | `cache_options: { expires_in: ... }` |
298+
| `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+
214304
## Cache Warming
215305

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

docs/pro/fragment-caching.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ end %>
4141

4242
A `cached_react_component_hash` variant is also available for cases where you need to extract metadata (like `<title>`) from the rendered output.
4343

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+
class Post < ApplicationRecord
65+
include ReactOnRailsPro::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+
4475
## Cache Warming
4576

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

llms-full-pro.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3999,6 +3999,37 @@ end %>
39993999

40004000
A `cached_react_component_hash` variant is also available for cases where you need to extract metadata (like `<title>`) from the rendered output.
40014001

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+
40024033
## Cache Warming
40034034

40044035
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

Comments
 (0)