Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Going further
  2. SSR and hydration

SSR and hydration#

Craft can render a complete application to deterministic HTML on the server, transfer its serializable state, and then attach the browser runtime to the existing DOM. This runtime path does not require the Craft compiler.

Use it when the first response must contain useful HTML without rebuilding the same component tree during browser startup.

Render one isolated request#

renderCraft creates a new platform, injector, primitive registry, in-memory history, and storage pair for every call. Do not reuse its injector between requests.

ts
import { renderCraft } from '@craft-ts/component';
import { appConfig } from './app.config';

const controller = new AbortController();
const rendered = await renderCraft({
  config: appConfig,
  url: '/dashboard?page=2',
  signal: controller.signal,
  timeoutMs: 5_000,
});

return new Response(
  `<!doctype html><html><body>${rendered.html}</body></html>`,
  {
    headers: { 'content-type': 'text/html; charset=utf-8' },
  },
);

The result separates rootHtml and the transfer snapshot; html combines both. renderToString(component, options) is the smaller API when no application config is needed.

All app initializers are run before the server render. Aborting the request rejects pending SSR work with the signal reason. A blocking render that exceeds timeoutMs rejects with CraftSsrTimeoutError and lists the pending sources. A route policy may set a shorter timeoutMs for the sources it blocks.

Hydrate the existing DOM#

Serve the generated <craft-root>, style element, and transfer script without changing them. The browser entry point then uses the same app config. startCraft chooses hydration when the SSR marker is present and falls back to a normal client mount when the page was not rendered by Craft SSR:

ts
import { startCraft } from '@craft-ts/component';
import { appConfig } from './app.config';

const app = startCraft({ config: appConfig });

Hydration restores the snapshot before creating primitives, claims elements, text markers, and block boundaries by their structural keys, and attaches bindings and listeners. A resolved transferred query therefore does not issue the same initial request again. The transfer script is removed after a successful first pass.

Call app.destroy() when the application host is removed. Pass host, snapshot, or onMismatch when the defaults are not appropriate.

Use hydrateCraft directly when the application needs to force hydration or pass hydration-specific options.

Choose what SSR does with pending data#

The boundary that owns the pending UI owns its SSR policy. A query still only describes data and its loader.

ts
div(UserList()).pipe(
  pendingNode({
    ssr: 'block',
    fallback: () => UserListSkeleton(),
  }),
);

The three modes are:

ModeServer actionInitial HTML
blockStarts and awaits the suspended sourceResolved content and query snapshot
fallbackDoes not await the sourceBoundary fallback
clientDoes not start the sourceExplicit browser-owned shell or fallback

client requires an explicit fallback in the catch-all form. An exhaustive boundary already supplies explicit source fallbacks.

A route can provide the page default:

ts
craftRoute('dashboard', {
  path: 'dashboard',
  loadComponent: () => import('./dashboard'),
  ssr: { mode: 'block' },
});

The nearest local pendingNode wins over the route policy. A read that suspends without either policy fails with CraftUnhandledSsrResolutionError; Craft never silently skips the loader or waits forever. Reloading queries keep rendering their previous value and do not suspend.

Structural identity and local recovery#

Hydration keys come from the component and template path, not from a global counter or random id. Static children use their logical position, blocks use a stable boundary segment, and forNode entries use the declared business key. Server and client must therefore execute the same template structure and use stable forNode keys.

If a key is absent, a tag differs, text changed, or a dynamic branch no longer matches, Craft recreates that local subtree and keeps compatible siblings. In development it also reports a HydrationMismatchError containing the key, expected node, actual node, and reason.

Transfer snapshot rules#

Only values composed of JSON primitives, plain objects, and arrays are transferred. Functions, bigint, class instances, and cycles fail explicitly. Unreadable or absent primitive values are omitted. Query entries include their status, resolved value when present, and a plain { name, message } error when the runtime exposes one.

serializeCraftTransferSnapshot escapes <, >, &, U+2028, and U+2029, so the JSON cannot close its application/json script element. Treat the snapshot as application data nevertheless: do not place secrets in state that reaches the browser.

Current scope#

This first runtime release covers full-page hydration, deterministic HTML, CSS collection, state/query transfer, async boundary policies, keyed forNode recovery, and local mismatch remounts. Streaming, resumability, islands, cross-boundary event replay, compiler-generated renderers, and a direct server-function transport are later work.

←Alchemy providerEffect integration→

On this page

  • Render one isolated request
  • Hydrate the existing DOM
  • Choose what SSR does with pending data
  • Structural identity and local recovery
  • Transfer snapshot rules
  • Current scope

@craft-ts · MIT licensed

GitHubnpm

Search