Cache

Use cache() for request-scoped data caching. Prevent redundant database queries and API calls within a single request — even when the same data is fetched from multiple components.

Core Feature

Overview

The cache() function wraps any async function to enable automatic request-scoped memoization. When multiple components call the same cached function with identical arguments during a single request, only the first call executes — subsequent calls return the cached result instantly.

ℹ️
Request-scoped, not global The cache lives only for the duration of one SSR render. Each new request starts with a fresh, empty cache. This prevents stale data from leaking between requests while eliminating redundant work within the same render tree.

Basic usage

Wrap your data fetching function with cache():

lib/db.tstypescript
import { cache } from 'nukejs'
import { prisma } from './prisma'

// Without cache: every call hits the database
export async function getUser(id: number) {
    return prisma.user.findUnique({ where: { id } })
}

// With cache: first call hits DB, subsequent calls return cached value
export const getCachedUser = cache(async (id: number) => {
    console.log('DB query for user', id)
    return prisma.user.findUnique({ where: { id } })
})

Example: Prevent N+1 queries

A common pattern: multiple components in the render tree need the same user data. Without caching, each component triggers a separate database query. With cache(), the query runs once and all components share the result.

app/pages/dashboard.tsxtypescript
import { getCachedUser } from '../../lib/db'
import ProfileHeader from '../../components/ProfileHeader'
import RecentActivity from '../../components/RecentActivity'
import Sidebar from '../../components/Sidebar'

export default async function DashboardPage() {
    const userId = 123

    // Three components each call getCachedUser(123)
    // Only the first call hits the database
    return (
        <div>
            <ProfileHeader userId={userId} />
            <RecentActivity userId={userId} />
            <Sidebar userId={userId} />
        </div>
    )
}
app/components/ProfileHeader.tsxtypescript
import { getCachedUser } from '../lib/db'

export default async function ProfileHeader({ userId }: { userId: number }) {
    // First component to call getCachedUser(123) — executes DB query
    const user = await getCachedUser(userId)
    return <h1>{user.name}</h1>
}
app/components/RecentActivity.tsxtypescript
import { getCachedUser } from '../lib/db'

export default async function RecentActivity({ userId }: { userId: number }) {
    // Second call with same userId — returns cached result, no DB query
    const user = await getCachedUser(userId)
    return <p>Recent posts by {user.name}</p>
}
app/components/Sidebar.tsxtypescript
import { getCachedUser } from '../lib/db'

export default async function Sidebar({ userId }: { userId: number }) {
    // Third call — also returns cached result
    const user = await getCachedUser(userId)
    return <aside>Logged in as {user.email}</aside>
}
💡
Console output With the console.log in getCachedUser, you'll see "DB query for user 123" printed only once per request, even though three components called the function.

Cache key behavior

The cache key is computed from the function arguments using JSON.stringify. Arguments must be JSON-serializable (strings, numbers, booleans, arrays, plain objects). Functions, symbols, and class instances cannot be cached and will bypass the cache silently.

lib/db.tstypescript
import { cache } from 'nukejs'

export const getPostsByTag = cache(async (tags: string[]) => {
    // Arrays are serialized for the cache key:
    // ['react', 'ssr'] and ['react', 'ssr'] → same key (cached)
    // ['react', 'ssr'] and ['ssr', 'react'] → different keys (separate calls)
    return db.posts.findMany({ where: { tags: { hasEvery: tags } } })
})

export const getPost = cache(async (id: number, options: { draft?: boolean }) => {
    // Object arguments work too:
    // (1, { draft: true }) and (1, { draft: true }) → same key
    // (1, { draft: true }) and (1, { draft: false }) → different keys
    return db.posts.findUnique({ where: { id }, include: { author: options.draft } })
})

Example: Cascading data dependencies

Use cache() to share intermediate results across unrelated parts of the component tree without prop drilling or global state:

lib/data.tstypescript
import { cache } from 'nukejs'
import { prisma } from './prisma'

export const getCurrentUser = cache(async (sessionToken: string) => {
    // Expensive session lookup
    const session = await prisma.session.findUnique({
        where: { token: sessionToken },
        include: { user: true }
    })
    return session?.user ?? null
})

export const getUserProjects = cache(async (sessionToken: string) => {
    // Reuses getCurrentUser result if already cached
    const user = await getCurrentUser(sessionToken)
    if (!user) return []
    return prisma.project.findMany({ where: { ownerId: user.id } })
})

export const getUserNotifications = cache(async (sessionToken: string) => {
    // Also reuses getCurrentUser result
    const user = await getCurrentUser(sessionToken)
    if (!user) return []
    return prisma.notification.findMany({
        where: { userId: user.id, read: false }
    })
})
app/pages/workspace.tsxtypescript
import { getCurrentUser, getUserProjects, getUserNotifications } from '../../lib/data'
import { useRequest } from 'nukejs'

export default async function WorkspacePage() {
    const { headers } = useRequest()
    const token = headers['x-session-token'] ?? ''

    // All three functions call getCurrentUser(token) internally,
    // but the session lookup runs only once
    const [user, projects, notifications] = await Promise.all([
        getCurrentUser(token),
        getUserProjects(token),
        getUserNotifications(token),
    ])

    if (!user) return <p>Please log in</p>

    return (
        <main>
            <h1>Welcome, {user.name}</h1>
            <p>You have {notifications.length} unread notifications</p>
            <ul>
                {projects.map(p => <li key={p.id}>{p.name}</li>)}
            </ul>
        </main>
    )
}

When to use cache()

Use caseBenefit
Shared data across multiple componentsFetch once at the top level, call everywhere without prop drilling
Cascading dependencies (user → projects → tasks)Each level can call the parent function; intermediate results are cached
Layout + page both need the same dataBoth can fetch independently; cache ensures only one network/DB call
Heavy computation (parsing, sorting, aggregation)Run once per request even if multiple components need the result

Limitations

  • Server-onlycache() works during SSR. It has no effect in client components or after hydration.
  • Arguments must be serializable — Functions, symbols, and complex objects are not cached. The call falls through to the original function.
  • Request-scoped — Cache entries do not persist across requests. For cross-request caching, use a separate solution like Redis or an in-memory LRU cache.
  • Async only — The function passed to cache() must return a Promise. Synchronous functions are not supported.
⚠️
Promise rejection invalidates the cache entry If the wrapped function throws or rejects, that specific cache entry is deleted immediately. A retry with the same arguments will execute the function again rather than returning a cached rejection.

TypeScript

The cache() function preserves the type signature of the wrapped function:

lib/data.tstypescript
import { cache } from 'nukejs'

// Fully typed, including parameters and return type
export const getUser = cache(async (id: number): Promise<User | null> => {
    return prisma.user.findUnique({ where: { id } })
})

// Usage is fully type-safe
const user = await getUser(123)       // user: User | null
const invalid = await getUser('abc')  // ❌ TypeScript error