Cache RSC Fragments with unstable_cache
React on Rails Pro provides an experimental unstable_cache API for caching a React Server
Component's serialized Flight payload. Use it for server components whose output can be reused for
the same arguments.
unstable_cache is available only in the React server bundle. Import it from
react-on-rails-pro/cache:
import { unstable_cache } from 'react-on-rails-pro/cache';
const ProductCard = unstable_cache(
async ({ productId }: { productId: string }) => {
const product = await loadProduct(productId);
return <ProductCardView product={product} />;
},
{
id: 'product-card',
revalidate: 60,
},
);
The example assumes the product output is shared across callers. A cache hit returns the stored
payload without running the callback, including loadProduct. Run authorization checks on every
request before invoking the cached function, not inside its callback. Derive tenant or user scope
from authenticated server context, and include any scope or personalization that changes the rendered
output in the function arguments. Cache-key scoping does not replace authorization.
The options are:
id(required): a stable identifier that is unique to each cached function sharing a handler within a build. Two functions with the same ID and arguments produce the same cache key.revalidate: the entry lifetime in seconds. The default0means that the cache handler does not expire the entry by time.kind: the registered cache-handler name. The default isdefault, which uses an in-memory LRU cache in each Node Renderer worker.
The cache key includes the build ID, function ID, and function arguments. Arguments must use the
supported deterministic value types. Supported built-in instances include Date, Map, and Set.
Circular references, functions, symbols, and arbitrary custom class instances are not supported.
Use Shared Storage
The default cache is process-local. Separate Node Renderer workers do not share its entries. For a
shared cache, install the optional ioredis peer dependency and register RedisCacheHandler:
In this example, REDIS_URL must identify a Redis database or instance dedicated to this application
and environment. Cache keys do not include application or environment identifiers automatically.
If you share a Redis database, instead pass an ioredis RedisOptions object as redisUrl, with an
application-and-environment-specific keyPrefix alongside its connection settings.
import { RedisCacheHandler, registerCacheHandler, unstable_cache } from 'react-on-rails-pro/cache';
registerCacheHandler('redis', new RedisCacheHandler({ redisUrl: process.env.REDIS_URL }));
const ProductCard = unstable_cache(renderProductCard, {
id: 'product-card',
kind: 'redis',
revalidate: 60,
});
You can also implement the exported CacheHandler interface and register it with
registerCacheHandler(kind, handler). A handler implements asynchronous get(key) and
set(key, entry) methods. TieredCacheHandler can compose handlers as L1 and L2 caches.
Custom handlers must enforce the entry lifetime: return null from get for stale entries based on
entry.timestamp and entry.revalidate, or enforce expiry with the storage backend's TTL.
unstable_cache replays any non-null entry returned by get; it does not check expiry itself.
Invalidation Limits
The RSC cache API does not currently provide tag-based invalidation. In particular, it does not
export unstable_revalidateTag, expose a Node Renderer tag-invalidation endpoint, or provide a Ruby
ReactOnRailsPro::RSCCache bridge. The CacheHandler interface also has no delete method.
Use a finite revalidate interval when data can change. Include all data that distinguishes the
rendered result in the cached function arguments. The build ID comes from the RSC artifact ID
(rscBundleHash), which is derived from the RSC bundle and its companion files. Automatic
build-scoped cache separation occurs only when that artifact ID changes; rebuilding or deploying
with unchanged RSC artifacts does not rotate it. Changes to Rails code, data, or configuration still
need a finite lifetime or an explicit version in the function id or arguments to separate cached
results. Changing the namespace does not delete old entries from shared storage.
React on Rails Pro's Ruby fragment-caching helpers have a separate cache_tags: and
ReactOnRailsPro.revalidate_tag API. That API invalidates Rails fragment-cache entries; it does not
invalidate entries created by JavaScript unstable_cache.