Skip to content

unstable_cache has no write-time invalidation: a committed Rails write stays invisible until TTL #5077

Description

@AbanoubGhadban

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions