Component Structure

Component Structure

Component Structure

Plugin DeveloperPlugin Developer Agent Kit — documentation written for AI agents, readable by humans.

Full-stack components use makeJayStackComponent with a fluent builder API and three rendering phases.

Basic Structure

import { makeJayStackComponent, phaseOutput } from '@jay-framework/fullstack-component';
import type { MyContract } from './my-contract.generated';

export const myComponent = makeJayStackComponent<MyContract>()
  .withSlowlyRender(async (props) => {
    const data = await fetchStaticData();
    return phaseOutput(
      { title: data.title, description: data.description }, // ViewState
      { productId: data.id }, // CarryForward
    );
  })
  .withFastRender(async (props) => {
    return phaseOutput({ price: await getPrice(), inStock: true }, {});
  })
  .withInteractive(function MyComponent(props, refs) {
    // Client-side hooks here
    return { render: () => ({}) };
  });

Builder API

.withProps<T>()

Declare the props type (must match contract props):

makeJayStackComponent<MyContract>().withProps<{ productId: string; currency?: string }>();

.withServices(...markers)

Inject server-side services:

.withServices(DATABASE_SERVICE, CACHE_SERVICE)
.withSlowlyRender(async (props, db, cache) => {
    // db and cache are injected
})

.withContexts(...markers)

Consume client-side contexts in the interactive phase:

.withContexts(CART_CONTEXT)
.withInteractive(function MyComp(props, refs, cartCtx) {
    // cartCtx available in interactive phase
})

.withLoadParams(fn)

Generate URL params for SSG (static site generation). Returns an async iterable of param arrays:

.withLoadParams(async function* (db) {
    const products = await db.getAllProducts();
    yield products.map(p => ({ slug: p.slug }));
})

.withSlowlyRender(fn) — Build-time rendering

Runs during SSG. Has access to props and services. Returns phaseOutput(viewState, carryForward).

CarryForward data is passed to the fast phase but not included in the ViewState.

.withSlowlyRender(async (props, db) => {
    const product = await db.getProduct(props.slug);
    if (!product) return notFound('Product not found');
    return phaseOutput(
        { title: product.name, description: product.desc },
        { productId: product.id },  // Available in fast phase
    );
})

.withFastRender(fn) — Request-time rendering

Runs on each request. Receives props (including query for query parameters and cookies for HTTP cookies) and carry-forward from slow phase. Can set HTTP response headers via phaseOutput() options.

.withFastRender(async (props, db) => {
    const price = await db.getPrice(props.carryForward.productId);
    return phaseOutput({ price, inStock: price > 0 }, {});
})

Cookies

props.cookies is a Record<string, string> parsed from the HTTP Cookie header. Use for auth checks:

.withFastRender(async (props, memberService) => {
    const token = props.cookies['session-token'];
    if (!token) return redirect3xx(302, '/login');

    const member = await memberService.validate(token);
    if (!member) return redirect3xx(302, '/login');

    return phaseOutput(
        { isLoggedIn: true, memberName: member.name },
        {},
        { responseHeaders: { 'Cache-Control': 'no-store' } },
    );
})
  • Empty {} when no cookies are present
  • Not available in the slow phase (compile error)
  • responseHeaders in phaseOutput() options sets HTTP headers on the response (e.g. Cache-Control: no-store for per-user pages)

.withClientDefaults(fn) — Defaults for dynamically created forEach items

Required only when the component is used inside a forEach where new items can be added on the client. When a user adds a new item to a forEach array, the new instance has no server data — withClientDefaults provides the initial ViewState.

.withClientDefaults((props) => ({
    viewState: { label: `Item ${props.itemId}`, value: 0 },
    carryForward: {},
}))

When to use: Component inside forEach + items can be added client-side. When NOT to use: Components outside forEach, or forEach with server-only items. Use withFastRender instead for SSR initial state.

.withInteractive(ComponentConstructor) — Client-side logic

The interactive phase runs in the browser. Use hooks here (see component-state.md):

.withInteractive(function ProductPage(props, refs) {
    const [quantity, setQuantity] = createSignal(1);

    refs.addToCart.onClick(() => {
        addToCartAction({ productId: props.productId, quantity: quantity() });
    });

    return {
        render: () => ({
            quantity: quantity(),
        }),
    };
})

Render Return Types

Each phase can return:

  • phaseOutput(viewState, carryForward, options?) — success (options: { headTags?, responseHeaders? })
  • notFound(), badRequest(), unauthorized(), forbidden() — client errors
  • serverError5xx(status, message) — server errors
  • redirect3xx(status, location) — redirects

See render-results.md for details.

Props and Contract Alignment

The component's props type must match the contract's props section:

# Contract
props:
  - name: productId
    type: string
    required: true
// Component
makeJayStackComponent<MyContract>().withProps<{ productId: string }>();

Params and Contract Alignment

If the contract has params, the component should use withLoadParams and the route must have matching dynamic segments:

# Contract
params:
  slug: string
// Component
.withLoadParams(async function* (db) {
    const items = await db.getAll();
    yield items.map(i => ({ slug: i.slug }));
})

About this document

This page is part of the Jay Stack Agent Kit — documentation generated from the framework source and written primarily for AI agents. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation. Learn more about the Agent Kit →