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>;
}