Skip to content

API Reference

All public exports from @cmj/juice/runtime and @cmj/juice/client, with when and why you would use each one.

@cmj/juice/runtime

createRouter(manifest, options?)

The core function. Takes a flight manifest and optional configuration, returns a WinterCG-compatible fetch handler.

import { createRouter } from '@cmj/juice/runtime';
import manifest from './flight-manifest.json';

const handler = createRouter(manifest, {
  root: import.meta.url,
  streaming: 'shell',
});

// handler: (req: Request) => Promise<Response>

When to use: always. This is the entry point of every Juice app. See Router Options for all configuration options.

redirect(url, status?)

Throws a redirect Response. Works in components, server actions, and middleware. Default status is 302 (Found).

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

// In a server component
export default async function Product({ params }: { params: { id: string } }) {
  const product = await db.find(params.id);
  if (product.archived) {
    redirect('/products');       // 302 redirect
  }
  if (!product) {
    redirect('/not-found', 303); // 303 redirect (POST-Redirect-GET)
  }
  return <h1>{product.name}</h1>;
}

// In a server action
async function login(formData: FormData) {
  'use server';
  // ... validate credentials
  redirect('/dashboard', 303);  // Always use 303 after form submissions
}

Coming from Next.js: same API. redirect() throws, so it must not be called inside a try/catch that swallows the error.

cache(fn)

Wraps an async function with request-scoped memoization. Deduplicates calls with the same arguments within a single request using identity-based keys.

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

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

// Two calls with the same userId in the same request = one DB query
const user1 = await getUser('42');
const user2 = await getUser('42'); // Returns cached promise, no second query

When to use: any async function called from multiple components in the same render. Typically data-fetching functions. Do not use for functions with side effects.

Coming from Next.js: Next.js patches fetch() for automatic deduplication. Juice does not touch fetch(). You explicitly wrap functions you want deduplicated.

setContext(req, key, value)

Attaches a value to the request's context store. The key can be a string or a typed ContextKey.

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

// With a string key (value type is unknown when reading)
setContext(req, 'userId', '42');

// With a typed key (value type is enforced)
import { createContextKey } from '@cmj/juice/runtime';
const userKey = createContextKey<User>('user');
setContext(req, userKey, user); // TypeScript enforces user: User

When to use: in middleware, to pass data to route components. Auth middleware sets the user, database middleware sets the connection pool.

getContext(req, key)

Reads a value from the request's context store. Returns T | undefinedwhen using a typed ContextKey<T>, or unknown with string keys.

import { getContext } from '@cmj/juice/runtime';
import { userKey } from '../context-keys';

export default function Dashboard({ request }: { request: Request }) {
  const user = getContext(request, userKey); // User | undefined
  return <h1>Welcome, {user?.name}</h1>;
}

createContextKey<T>(name)

Creates a typed context key. The name parameter is used for debugging. The type parameter T is the value type, enforced at compile time. At runtime, this is just an object with a name property.

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

export const userKey = createContextKey<User>('user');
export const dbKey = createContextKey<Database>('db');
export const requestIdKey = createContextKey<string>('requestId');

createActionContext(req, params?)

Creates an ActionContext for testing server actions outside of a real request. Useful in unit tests.

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

// In a test
const ctx = createActionContext(
  new Request('http://localhost/product/42', { method: 'POST' }),
  { id: '42' }
);
const result = await myAction(formData, ctx);

Type Exports (@cmj/juice/runtime)

TypeDescriptionWhen You Need It
FlightManifestThe manifest generated by the Vite plugin at build time.Rarely. Only if you are building tooling around the manifest.
RouterOptionsConfiguration for createRouter().When you want to type-check your options object separately.
ActionContext<TRoute>Second argument to server actions. Provides request, cookies, typed params, url.When your server action needs access to the request or route params.
ParsedBodyDiscriminated union: form | json | text.When you need to handle different content types in a single action.
ExtractRouteParams<T>Type-level extraction of route params from a pattern string.When building generic utilities that work with any route pattern.
PagePropsInterface augmented by juice-env.d.ts. Indexed by route pattern.Every route component. PageProps['/product/:id'] gives typed params.
JuiceMiddleware(req: Request, next: () => Promise<Response>) => Response | Promise<Response>When typing a middleware function explicitly.
ContextKey<T>Branded key carrying value type at compile time. Runtime: { name: string }.When defining typed context keys with createContextKey.
CacheAdapterInterface for cross-request cache: get(key) and set(key, value, ttl?).When implementing a custom cache adapter (Redis, KV, etc.).

@cmj/juice/client

Link

Type-safe navigation component. Renders a standard <a> tag with SPA navigation via the Navigation API. Prefetches the RSC payload on hover by default.

import { Link } from '@cmj/juice/client';

<Link href="/product/42">View Product</Link>
<Link href="/about" prefetch="viewport">About</Link>
<Link href="/login" replace>Log In</Link>
<Link href="/settings" prefetch="none">Settings</Link>

Props extend standard <a> attributes plus: prefetch('hover' | 'viewport' | 'none', default 'hover') and replace (replace history entry instead of push).

useRouter()

Hook for programmatic navigation. Returns an object with pathname, push(), replace(), and prefetch().

'use client';
import { useRouter } from '@cmj/juice/client';

export function LogoutButton() {
  const router = useRouter();

  async function handleLogout() {
    await fetch('/api/logout', { method: 'POST' });
    router.push('/login');
  }

  return <button onClick={handleLogout}>Log out</button>;
}

initNavigation(setPage, options?)

Bootstraps the Navigation API interceptor for SPA transitions. Called once in your client entry point. Returns a cleanup function that removes the interceptor.

import { initNavigation } from '@cmj/juice/client';

// In your client entry
const cleanup = initNavigation(setPage, {
  viewTransitions: true,
  prefetch: false,
  callServer: customCallServer, // optional
});

// cleanup() removes the navigation interceptor

setPage is a React state setter that receives the new RSC tree. initNavigation intercepts all same-origin navigations using the Navigation API. When a navigation occurs, it fetches the RSC payload (with Accept: text/x-component and X-Juice-Segment: pageheaders), decodes the response, and calls setPage with the new tree.

If the Navigation API is not available (older browsers), navigation falls back to full page reloads. No polyfill is needed -- the app still works, just without SPA transitions.

NavigationOptions:

OptionTypeDefaultDescription
viewTransitionsbooleantrueUse the View Transitions API for smooth page transitions. Falls back to instant swap in browsers that do not support it.
prefetchbooleanfalseEnable link prefetching on hover/focus globally. Individual Link components can override this.
callServerfunctionbuilt-inCustom server action RPC handler. Receives the action ID and arguments, returns the action result. See Server Actions for the default behavior.

When to use: only in custom client entry points. The scaffolded project sets this up for you. You touch this when you need to customize view transitions, override callServer, or intercept navigation events.

callServer (default behavior)

The built-in callServer handles server action RPC in the browser. When a server action is invoked, it:

  1. POSTs to the current URL with an x-juice-action-id header containing the action's ID.
  2. Sends the serialized arguments as the request body.
  3. Checks the response Content-Type:
  • text/x-component -- decodes via createFromFetch from react-server-dom-webpack/client. This triggers RSC reconciliation, which is why useOptimistic and useActionState work automatically.
  • application/json -- decodes via response.json(). For actions that return plain data without re-rendering.

You can replace this with a custom callServer in initNavigationoptions for custom headers, retry logic, or error reporting.

prefetchRSC(url)

Manually prefetches the RSC payload for a URL. The payload is cached for 30 seconds, and subsequent calls with the same URL within that window are idempotent (no duplicate fetches). When the user navigates to a prefetched URL, the cached payload is used immediately -- no network request.

import { prefetchRSC } from '@cmj/juice/client';

// Prefetch the next page when the user starts typing
function SearchInput() {
  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const query = e.target.value;
    if (query.length > 2) {
      prefetchRSC(`/search?q=${encodeURIComponent(query)}`);
    }
  }

  return <input onChange={handleChange} placeholder="Search..." />;
}

// Prefetch a known destination ahead of time
prefetchRSC('/onboarding/step-2');

// Calling again within 30s is a no-op
prefetchRSC('/onboarding/step-2'); // idempotent

When to use: when you know the user is likely to navigate somewhere but there is no Link element to attach prefetching to. Examples: search results, keyboard navigation, wizard flows, post-login redirects.

Client Type Exports

TypeDescription
LinkPropsProps for the Link component. Extends standard anchor attributes plus prefetch and replace.
PrefetchStrategy'hover' | 'viewport' | 'none'
NavigationOptionsOptions for initNavigation(): viewTransitions, prefetch, and callServer.
RouterReturn type of useRouter(): pathname, push(), replace(), prefetch().

When NOT to Use These APIs

If you are building a page with no navigation (a single dashboard, an embedded widget), you do not need Link, useRouter, or initNavigation. These are SPA navigation primitives. Without multiple pages to navigate between, they add unused code to your client bundle.