Laravel-inspired caching system with multiple drivers for React applications.
Built on top of @stackra/ts-container for seamless dependency injection.
- Features
- Installation
- Quick Start
- Configuration
- Cache Drivers
- Usage
- React Hooks
- API Reference
- Custom Stores
- TypeScript
- Laravel Comparison
- Requirements
- License
- Multiple Drivers — Memory, Redis, and Null stores out of the box
- Laravel-Inspired API —
remember,rememberForever,pull,tags, and more - Cache Tagging — Group related cache entries and flush them together (Redis)
- React Hooks —
useCache()anduseCachedQuery()for component-level caching - Dependency Injection — First-class DI support via
@stackra/ts-container - Type-Safe Configuration —
defineConfig()helper with full autocomplete - TTL Support — Per-operation and per-store default TTL
- Key Prefixing — Global and per-store prefixes to avoid collisions
- LRU Eviction — Memory store supports max size with automatic eviction
- Lazy Initialization — Stores are created on first use
- Zero Config — Works with sensible defaults, customize when needed
# npm
npm install @stackra/ts-cache @stackra/ts-container
# pnpm
pnpm add @stackra/ts-cache @stackra/ts-container
# yarn
yarn add @stackra/ts-cache @stackra/ts-containerFor Redis support, also install:
pnpm add @stackra/ts-redisimport { Module } from '@stackra/ts-container';
import { CacheModule } from '@stackra/ts-cache';
@Module({
imports: [
CacheModule.forRoot({
default: 'memory',
stores: {
memory: {
driver: 'memory',
maxSize: 1000,
ttl: 300,
},
},
}),
],
})
export class AppModule {}Then use it anywhere via DI or hooks:
import { useCache } from '@stackra/ts-cache';
function UserProfile({ userId }: { userId: string }) {
const cache = useCache();
useEffect(() => {
async function load() {
const user = await cache.remember(`user:${userId}`, 3600, () =>
fetch(`/api/users/${userId}`).then((r) => r.json())
);
setUser(user);
}
load();
}, [userId]);
// ...
}The configuration object follows the same structure as Laravel's
config/cache.php:
import { CacheModule } from '@stackra/ts-cache';
CacheModule.forRoot({
// Default store used when none is specified
default: 'memory',
// All available stores
stores: {
memory: {
driver: 'memory',
maxSize: 1000,
ttl: 300, // 5 minutes
prefix: 'mem_',
},
redis: {
driver: 'redis',
connection: redisClient,
prefix: 'cache_',
ttl: 3600, // 1 hour
},
null: {
driver: 'null',
},
},
// Global key prefix (applied to all stores)
prefix: 'myapp_',
});For type-safe configuration with IDE autocomplete, use the defineConfig
helper:
// cache.config.ts
import { defineConfig } from '@stackra/ts-cache';
export default defineConfig({
default: 'memory',
stores: {
memory: {
driver: 'memory',
maxSize: 1000,
ttl: 300,
},
redis: {
driver: 'redis',
connection: redisClient,
prefix: 'cache_',
ttl: 3600,
},
},
prefix: 'app_',
});The included config/cache.config.ts supports Vite environment variables:
| Variable | Description | Default |
|---|---|---|
VITE_CACHE_DRIVER |
Default cache driver | 'memory' |
CACHE_PREFIX |
Global key prefix | 'app_' |
VITE_CACHE_MEMORY_MAX_SIZE |
Memory store max entries | 1000 |
VITE_CACHE_MEMORY_TTL |
Memory store TTL (seconds) | 300 |
VITE_REDIS_CACHE_CONNECTION |
Redis connection name | 'cache' |
VITE_CACHE_REDIS_PREFIX |
Redis key prefix | 'cache_' |
VITE_CACHE_REDIS_TTL |
Redis TTL (seconds) | 3600 |
Fast in-memory cache using JavaScript Map. Data is lost on page refresh or
process restart.
{
driver: 'memory',
maxSize: 1000, // Max entries before LRU eviction (optional)
ttl: 300, // Default TTL in seconds (optional)
prefix: 'mem_', // Key prefix (optional)
}| Option | Type | Default | Description |
|---|---|---|---|
driver |
'memory' |
— | Required driver identifier |
maxSize |
number |
undefined |
Max entries (unlimited if omitted) |
ttl |
number |
300 |
Default TTL in seconds |
prefix |
string |
'' |
Key prefix |
Best for: development, client-side caching, temporary data.
Persistent cache backed by Redis. Supports tagging, distributed caching, and atomic operations.
{
driver: 'redis',
connection: redisClient, // RedisConnection instance
prefix: 'cache_', // Key prefix (optional)
ttl: 3600, // Default TTL in seconds (optional)
}| Option | Type | Default | Description |
|---|---|---|---|
driver |
'redis' |
— | Required driver identifier |
connection |
RedisConnection |
— | Redis client instance |
ttl |
number |
300 |
Default TTL in seconds |
prefix |
string |
'' |
Key prefix |
Best for: production, distributed systems, when persistence or tagging is needed.
No-op store that never caches anything. All writes succeed, all reads return
undefined.
{
driver: 'null',
}Best for: testing, disabling cache, benchmarking without cache overhead.
const cache = useCache();
// Store a value (TTL in seconds)
await cache.put('user:123', { name: 'John' }, 3600);
// Retrieve a value
const user = await cache.get('user:123');
// Retrieve with a default
const name = await cache.get('user:name', 'Guest');
// Check existence
if (await cache.has('user:123')) {
// ...
}
// Store multiple values
await cache.putMany({ 'user:1': user1, 'user:2': user2 }, 3600);
// Retrieve multiple values
const users = await cache.many(['user:1', 'user:2']);
// Store only if key doesn't exist
const added = await cache.add('user:123', user, 3600);
// Store indefinitely (no expiration)
await cache.forever('config:app', appConfig);
// Remove a value
await cache.forget('user:123');
// Get and remove in one call
const token = await cache.pull('auth:token');
// Clear everything
await cache.flush();The remember method retrieves from cache or executes a callback and stores the
result — the most common caching pattern:
// Cache for 1 hour, fetch from DB on miss
const user = await cache.remember('user:123', 3600, async () => {
return await database.users.findById(123);
});
// Cache forever
const config = await cache.rememberForever('app:config', async () => {
return await loadConfigFromFile();
});Switch between stores at runtime:
const cache = useCache();
// Use default store
await cache.put('key', 'value', 3600);
// Use a specific store
const redisCache = cache.store('redis');
await redisCache.put('key', 'value', 3600);
const memoryCache = cache.store('memory');
await memoryCache.put('key', 'value', 300);
// Or via the hook
const redis = useCache('redis');Atomic counter operations:
await cache.increment('page:views'); // 1
await cache.increment('page:views', 10); // 11
await cache.decrement('stock:item:42'); // -1
await cache.decrement('stock:item:42', 5); // -6Group related cache entries with tags for bulk invalidation. Tagging is only available with the Redis store.
const cache = useCache('redis');
// Store with tags
await cache.tags(['users', 'premium']).put('user:123', user, 3600);
await cache.tags(['users']).put('user:456', user2, 3600);
await cache.tags(['posts']).put('post:1', post, 3600);
// Retrieve tagged items
const user = await cache.tags(['users', 'premium']).get('user:123');
// Flush all entries tagged with 'users'
await cache.tags(['users']).flush();
// user:123 and user:456 are now gone, post:1 is untouched
// Flush specific tag combination
await cache.tags(['users', 'premium']).flush();How tagging works under the hood:
- Each tag gets a unique namespace ID stored in Redis
- Cache keys are prefixed with the combined namespace (e.g.,
abc123|def456:user:123) - Flushing a tag regenerates its namespace ID, making all old keys inaccessible
- Expired entries are tracked in Redis sorted sets for cleanup
Access the cache service from any React component:
import { useCache } from '@stackra/ts-cache';
function Dashboard() {
const cache = useCache();
const loadStats = async () => {
return cache.remember('dashboard:stats', 600, async () => {
const res = await fetch('/api/stats');
return res.json();
});
};
// ...
}
// Use a specific store
function Widget() {
const memoryCache = useCache('memory');
// ...
}A React Query-like hook that caches async query results:
import { useCachedQuery } from '@stackra/ts-cache';
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error, refetch, invalidate } = useCachedQuery({
key: `user:${userId}`,
queryFn: async () => {
const res = await fetch(`/api/users/${userId}`);
return res.json();
},
ttl: 3600,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<button onClick={refetch}>Refresh</button>
<button onClick={invalidate}>Clear Cache & Refresh</button>
</div>
);
}| Option | Type | Default | Description |
|---|---|---|---|
key |
string |
— | Cache key (required) |
queryFn |
() => Promise<T> |
— | Async function to fetch data (required) |
ttl |
number |
300 |
TTL in seconds |
storeName |
string |
default store | Which store to use |
enabled |
boolean |
true |
Enable/disable the query |
refetchOnMount |
boolean |
false |
Force refetch on mount |
refetchInterval |
number |
— | Auto-refetch interval (ms) |
| Property | Type | Description |
|---|---|---|
data |
T | undefined |
The cached/fetched data |
isLoading |
boolean |
Loading state |
error |
Error | null |
Error state |
refetch |
() => Promise<void> |
Re-run query (uses cache) |
invalidate |
() => Promise<void> |
Clear cache and re-run query |
The main service providing all cache operations.
| Method | Signature | Description |
|---|---|---|
get |
get<T>(key, defaultValue?): Promise<T | undefined> |
Retrieve a cached value |
many |
many<T>(keys): Promise<Record<string, T>> |
Retrieve multiple values |
put |
put<T>(key, value, ttl?): Promise<boolean> |
Store a value |
putMany |
putMany<T>(values, ttl?): Promise<boolean> |
Store multiple values |
add |
add<T>(key, value, ttl?): Promise<boolean> |
Store only if key doesn't exist |
has |
has(key): Promise<boolean> |
Check if key exists |
increment |
increment(key, value?): Promise<number> |
Increment a numeric value |
decrement |
decrement(key, value?): Promise<number> |
Decrement a numeric value |
forever |
forever<T>(key, value): Promise<boolean> |
Store indefinitely |
remember |
remember<T>(key, ttl, callback): Promise<T> |
Get or compute and store |
rememberForever |
rememberForever<T>(key, callback): Promise<T> |
Get or compute and store forever |
pull |
pull<T>(key, defaultValue?): Promise<T | undefined> |
Get and remove |
forget |
forget(key): Promise<boolean> |
Remove a value |
flush |
flush(): Promise<boolean> |
Clear all values |
tags |
tags(names): TaggedCache |
Get tagged cache (Redis only) |
store |
store(name?): CacheService |
Switch to a different store |
getDefaultStoreName |
getDefaultStoreName(): string |
Get default store name |
getStoreNames |
getStoreNames(): string[] |
List all configured stores |
hasStore |
hasStore(name): boolean |
Check if store is configured |
getPrefix |
getPrefix(): string |
Get global key prefix |
All stores implement this interface:
interface Store {
get(key: string): Promise<any>;
many(keys: string[]): Promise<Record<string, any>>;
put(key: string, value: any, seconds: number): Promise<boolean>;
putMany(values: Record<string, any>, seconds: number): Promise<boolean>;
increment(key: string, value?: number): Promise<number | boolean>;
decrement(key: string, value?: number): Promise<number | boolean>;
forever(key: string, value: any): Promise<boolean>;
forget(key: string): Promise<boolean>;
flush(): Promise<boolean>;
getPrefix(): string;
}Returned by cache.tags([...]). Same API as Store with tag-scoped keys:
interface TaggedCache {
get<T>(key: string): Promise<T | undefined>;
many(keys: string[]): Promise<Record<string, any>>;
put(key: string, value: any, ttl?: number): Promise<boolean>;
putMany(values: Record<string, any>, ttl?: number): Promise<boolean>;
increment(key: string, value?: number): Promise<number | boolean>;
decrement(key: string, value?: number): Promise<number | boolean>;
forever(key: string, value: any): Promise<boolean>;
forget(key: string): Promise<boolean>;
flush(): Promise<boolean>;
getTags(): TagSet;
}Implement the Store interface to create your own cache driver:
import type { Store } from '@stackra/ts-cache';
export class LocalStorageStore implements Store {
async get(key: string): Promise<any> {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : undefined;
}
async put(key: string, value: any, seconds: number): Promise<boolean> {
localStorage.setItem(
key,
JSON.stringify({ value, expires: Date.now() + seconds * 1000 })
);
return true;
}
// ... implement remaining methods
}The package is fully typed. All interfaces and types are exported:
import type {
CacheModuleOptions,
CacheServiceInterface,
Store,
TaggableStore,
TaggedCache,
TagSet,
MemoryStoreConfig,
RedisStoreConfig,
NullStoreConfig,
RedisConnection,
CacheDriver,
StoreConfig,
UseCachedQueryOptions,
UseCachedQueryResult,
} from '@stackra/ts-cache';If you're coming from Laravel, here's how the API maps:
| Laravel | ts-cache |
|---|---|
Cache::get('key') |
cache.get('key') |
Cache::put('key', $val, 3600) |
cache.put('key', val, 3600) |
Cache::remember('key', 3600, fn) |
cache.remember('key', 3600, fn) |
Cache::rememberForever('key', fn) |
cache.rememberForever('key', fn) |
Cache::forget('key') |
cache.forget('key') |
Cache::pull('key') |
cache.pull('key') |
Cache::flush() |
cache.flush() |
Cache::has('key') |
cache.has('key') |
Cache::add('key', $val, 3600) |
cache.add('key', val, 3600) |
Cache::forever('key', $val) |
cache.forever('key', val) |
Cache::increment('key') |
cache.increment('key') |
Cache::decrement('key') |
cache.decrement('key') |
Cache::store('redis') |
cache.store('redis') |
Cache::tags(['users'])->get() |
cache.tags(['users']).get() |
config/cache.php |
defineConfig({...}) |
- Node.js >= 18.0.0
- React 18 or 19
@stackra/ts-container(dependency injection)@stackra/ts-redis(optional, for Redis driver)