Page Components

Page Components

Page Components

DeveloperDeveloper Agent Kit — documentation written for AI agents, readable by humans.

A page component (page.ts) uses makeJayStackComponent to provide page-level data across rendering phases.

Basic Page Component

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

export const page = makeJayStackComponent<HomePageContract>()
  .withSlowlyRender(async () => {
    return phaseOutput({ heroTitle: 'Welcome', heroSubtitle: 'Build something great' }, {});
  })
  .withFastRender(async () => {
    return phaseOutput({ featuredCount: 12 }, {});
  });

The export name must be page for page-level components.

Page Component with Params

For dynamic routes, use withLoadParams and access params via props:

export const page = makeJayStackComponent<ProductPageContract>()
  .withServices(PRODUCTS_DB)
  .withLoadParams(async function* (db) {
    const products = await db.getAll();
    yield products.map((p) => ({ slug: p.slug }));
  })
  .withSlowlyRender(async (props, db) => {
    const product = await db.getBySlug(props.slug);
    if (!product) return notFound('Product not found');
    return phaseOutput(
      { title: product.name, description: product.desc },
      { productId: product.id },
    );
  })
  .withFastRender(async (props, db) => {
    const price = await db.getPrice(props.carryForward.productId);
    return phaseOutput({ price, inStock: price > 0 }, {});
  });

Page Component with Interactive Phase

Add client-side interactivity:

export const page = makeJayStackComponent<ProductPageContract>()
  .withServices(PRODUCTS_DB)
  .withSlowlyRender(async (props, db) => {
    // ... slow render
  })
  .withFastRender(async (props, db) => {
    // ... fast render
  })
  .withInteractive(function ProductPage(props, refs) {
    const [quantity, setQuantity] = createSignal(1);

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

    refs.quantityInput.exec$((input) => {
      input.addEventListener('change', (e) => {
        setQuantity(parseInt((e.target as HTMLInputElement).value));
      });
    });

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

DOM access in interactive phase

Reach rendered elements through refs only. Put overlays, panels, and lists in jay-html; drive them with ViewState from .withInteractive. Do not use document.querySelector, imperative createElement UI, or document-level drag listeners.

See component-refs.md.

Calling File Upload Actions

Actions created with .withFiles() accept browser File objects directly. Use oninput events on file inputs to drive signals, then pass them to the action:

import { createSignal } from '@jay-framework/component';
import { uploadPhoto } from '../actions/upload.actions';

.withInteractive(function UploadPage(props, refs, fastViewState) {
    const [result, setResult] = createSignal('');
    const [selectedFile, setSelectedFile] = createSignal<File | undefined>(undefined);

    refs.fileInput.oninput(({ event }) => {
        setSelectedFile((event.target as HTMLInputElement).files?.[0]);
    });

    refs.uploadBtn.onclick(async () => {
        const file = selectedFile();
        if (!file) return;
        const res = await uploadPhoto({ caption: 'My photo', photo: file });
        setResult(res.message);
    });

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

No casting needed — browser File is assignable to JayFile.

Combining with Headless Plugins

A page component handles page-level data. Plugin headless components handle their own data independently. Both render into the same page:

src/pages/products/[slug]/
├── page.jay-html          # Template: binds to both page + plugin data
├── page.jay-contract      # Page-level contract (title, breadcrumbs, etc.)
├── page.ts                # Page component

The jay-html template uses unprefixed bindings for page data and key-prefixed bindings for plugin data.

Note on .withClientDefaults()

withClientDefaults is only needed when a headless component is used inside a forEach and new items can be added on the client (e.g., "Add Item" button). It provides initial ViewState for instances that don't exist during SSR.

You do NOT need it for:

  • Components outside forEach — withFastRender provides SSR initial state
  • Components inside a conditional (if=) — server data is computed for all discovered instances regardless of the condition's SSR value
  • Static forEach where all items come from the server

Builder API Reference

See the plugin component-structure.md for the full builder API: .withProps(), .withServices(), .withContexts(), phase rendering, and render results.

State Hooks Reference

See component-state.md for createSignal, createMemo, createEffect, and other hooks used in the interactive phase.


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 →