GitHubnpm

Data Fetching

Server components in Juice are async. Fetch data directly in your components with request-scoped caching that deduplicates identical calls.

Async Server Components

Every route component runs on the server. You can await any async operation directly in the component body, including database queries, API calls, and file reads.

// app/routes/blog/[slug].tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await db.posts.findBySlug(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

Request-Scoped cache()

The cache() function wraps any async function and deduplicates calls with the same arguments within a single request. Keys are identity-based, not serialized, so objects like Request work as arguments without collision.

import { cache } from '@cmj/juice/runtime';

const getUser = cache(async (userId: string) => {
  return await db.users.find(userId);
});

// These two calls resolve to the same promise within one request
export default async function Profile({ params }: { params: { id: string } }) {
  const user = await getUser(params.id);
  return <UserCard user={user} />;
}

// In a sibling component rendered in the same request
async function UserSidebar({ userId }: { userId: string }) {
  const user = await getUser(userId); // Deduplicated — no second DB call
  return <aside>{user.name}</aside>;
}

How Identity-Based Keys Work

Primitives (strings, numbers, booleans) are compared by value. Objects and functions are compared by reference using a WeakMap of monotonic IDs. This means passing a Request object as an argument works correctly without producing key collisions from failed JSON serialization.

Cross-Request CacheAdapter

For caching across requests (CDN-level, Redis, KV), pass a cache adapter to createRouter():

import { createRouter } from '@cmj/juice/runtime';

createRouter(manifest, {
  cache: {
    get: (key) => kv.get(key),
    set: (key, value, ttl) => kv.set(key, value, { ttl }),
  },
});

Response Headers

Control caching and other headers per route using the response export:

export const response = {
  headers: {
    'Cache-Control': 'public, max-age=3600, s-maxage=86400',
  },
};

Metadata and SEO

Set the page title and meta tags via the response.head export. The runtime injects these into the HTML <head>.

export const response = {
  head: {
    title: 'Blog Post Title — My Site',
    description: 'A description for search engines and social cards.',
  },
};

Static Prerendering

Export prerender = true to mark a route as static. The runtime sets Cache-Control: public, max-age=31536000, immutable on the response.

export const prerender = true;

export default function About() {
  return <h1>About Us</h1>;
}