Summary
unstable_cache / cacheComponent's options are TTL-only —
{ id, revalidate = 0, kind } (lib/cache/unstable_cache.js:42, 17.0.1). There are no tags, no
delete(), no handler-level invalidate. The Rails-side fragment-cache path has exactly this
capability (cache_tags: + ReactOnRailsPro.revalidate_tag + the Cache::Revalidates
after_commit concern); the JS-side renderer cache has nothing a write can reach.
Where it does and does not bite (both empirically verified)
buildCacheKey(buildId, id, args) hashes the cached function's arguments
(lib/cache/buildCacheKey.js:90), so:
- Push-mode pages (data fetched by Rails and passed INTO the cached component as props):
a write changes the args → new key → miss → fresh render. No stale reads — but also no saved
Rails work; the cache only memoizes React rendering of unchanged data.
- Pull-mode pages (cache key is a stable identifier like
{productId}, data fetched INSIDE
the cached function): the cache prevents the fetch itself, so a committed write is invisible
until revalidate expires. Measured in the demo app (60s TTL): write committed at T+0; page
reload at T+8s still showed the old list (the renderer never asked Rails — the uncached variant
of the same page showed the new row at the same instant); the write appeared only on a reload
past the TTL window.
The second case is the entire value proposition of the cache (skip the query), so "just pass the
data as args" is not an answer — it deletes the benefit to restore correctness.
Ask
Tag-based invalidation on the JS-side cache, mirroring the Rails-side surface:
unstable_cache(fn, { id, revalidate, tags: ['product-1-reviews'] })
plus an invalidation entry point a Rails write can call — ideally the existing
ReactOnRailsPro.revalidate_tag fanning out to the renderer's cache handler (the
CacheHandler interface would grow deleteByTag/revalidateTag), so one after_commit busts
both caches. This is the same shape as Next.js revalidateTag/updateTag, and it is the one
read-your-writes piece that server functions would not have fixed either (the write lives in
Rails; the cache lives in the renderer; nothing connects them today).
Related context: react_on_rails#4874 (read-your-writes named as a remaining value item);
demo evidence in react-on-rails-demo-marketplace-rsc#245, docs/rsc-read-your-writes.md (config 3,
pull-mode-staleness artifacts).
Summary
unstable_cache/cacheComponent's options are TTL-only —{ id, revalidate = 0, kind }(lib/cache/unstable_cache.js:42, 17.0.1). There are no tags, nodelete(), no handler-level invalidate. The Rails-side fragment-cache path has exactly thiscapability (
cache_tags:+ReactOnRailsPro.revalidate_tag+ theCache::Revalidatesafter_commitconcern); the JS-side renderer cache has nothing a write can reach.Where it does and does not bite (both empirically verified)
buildCacheKey(buildId, id, args)hashes the cached function's arguments(
lib/cache/buildCacheKey.js:90), so:a write changes the args → new key → miss → fresh render. No stale reads — but also no saved
Rails work; the cache only memoizes React rendering of unchanged data.
{productId}, data fetched INSIDEthe cached function): the cache prevents the fetch itself, so a committed write is invisible
until
revalidateexpires. Measured in the demo app (60s TTL): write committed at T+0; pagereload at T+8s still showed the old list (the renderer never asked Rails — the uncached variant
of the same page showed the new row at the same instant); the write appeared only on a reload
past the TTL window.
The second case is the entire value proposition of the cache (skip the query), so "just pass the
data as args" is not an answer — it deletes the benefit to restore correctness.
Ask
Tag-based invalidation on the JS-side cache, mirroring the Rails-side surface:
plus an invalidation entry point a Rails write can call — ideally the existing
ReactOnRailsPro.revalidate_tagfanning out to the renderer's cache handler (theCacheHandlerinterface would growdeleteByTag/revalidateTag), so oneafter_commitbustsboth caches. This is the same shape as Next.js
revalidateTag/updateTag, and it is the oneread-your-writes piece that server functions would not have fixed either (the write lives in
Rails; the cache lives in the renderer; nothing connects them today).
Related context: react_on_rails#4874 (read-your-writes named as a remaining value item);
demo evidence in react-on-rails-demo-marketplace-rsc#245,
docs/rsc-read-your-writes.md(config 3,pull-mode-staleness artifacts).