Browse documentation

Template authoring

What a template can render

See how ordinary values, Effect values, streams, arrays, and renderer output become one template part without losing errors or requirements.

A saved-article page needs several kinds of output: a fixed heading, a query that changes, data loaded through a service, a group of links, and perhaps an empty state. They do not need to be converted into one special component-object format. Typed accepts ordinary values and Effect producers, then interprets them according to the position where they are rendered.

Start with Authoring Typed templates. This article answers the next design question: what should a component return, or pass into an interpolation, when its data isn’t simply a string?

Distinguish a value from the producer of that value

A string already exists. An Effect describes work that can obtain a value once. An Fx or Stream describes values arriving over time. The following view puts each into a different part:

import { Effect, Stream } from "effect";
import { Fx } from "@typed/fx";
import { html } from "@typed/template";

const owner = Effect.succeed("Ada");
const query = Fx.fromIterable(["", "scope"]);
const syncStatus = Stream.fromIterable(["Checking", "Up to date"]);

export const header = html`<header>
  <h1>${owner}'s saved articles</h1>
  <p>Query: ${query}</p>
  <output>${syncStatus}</output>
</header>`;

In a DOM render, each query emission updates its own position; it does not append another paragraph. The Effect runs when rendering starts, not when the module defines owner. A real RefSubject can replace the short demonstration Fx without changing the template’s interpolation syntax.

A server response needs a finite result. Its HTML renderer takes initial ordinary producer values rather than waiting forever for future changes. A source with no initial value can therefore stall server output. Rendering HTML on the server explains that boundary and the distinct protocol for ordered renderer-owned HTML chunks.

Let the position decide the interpretation

A value accepted by the broad Renderable contract is not meaningful in every position. An object can describe .data keys or a DOM property; that does not turn an arbitrary record into child markup.

InputIn a child positionIn a named element part
string, number, biginttextserialized attribute or direct property value
booleantext, including "false"attribute string or boolean-part truthiness
null or undefinedempty outputattribute removal or direct property assignment
arrayordered normalized entriesclass collection or the property’s array value
OptionNone is empty; Some selects its contained valueinterpreted by the chosen part
Effectobtain its result, then normalize itobtain the field’s value
Fx, Stream, RefSubjectupdate the local output as values arriveupdate the captured field
nested template or RenderEventrenderer output in this positionnot a generic attribute value

Unlike conditional JSX conventions, a boolean child is displayed data. Return null or the desired view when a condition should hide content. Arrays are recognized; arbitrary Sets, generators, and iterables are not implicitly rendered collections. Convert a fixed iterable to an array deliberately.

Return the output that fits the component

A helper does not need an extra element merely to return several pieces, and a helper with no setup does not need a component generator:

import { html } from "@typed/template";

export const EmptyResults = (query: string, canSave: boolean) => {
  return [
    html`<h2>No saved articles match ${query}</h2>`,
    canSave ? html`<p>Save an article to include it in future searches.</p>` : null,
  ];
};

export const ReadOnlyNotice = "This collection is read-only.";

Interpolate EmptyResults(query, canSave) as an ordered group. No wrapper is added around the heading and paragraph. When setup does need a generator, component accepts these same return forms directly: a template, ordinary data, an Effect, or native output. Returning a DOM node deliberately makes that component browser-specific; renderer neutrality depends on its actual values and services, not on the constructor alone.

An array describes order, not the enduring identity of domain records. If saved articles can be inserted, removed, or reordered, use keyed collections.

Keep errors and service requirements attached to the view

Loading a collection name can fail or require a service. Preserve those facts instead of starting an untracked Promise while constructing the template:

import { Context, Data, Effect } from "effect";
import { html } from "@typed/template";

class CollectionMissing extends Data.TaggedError("CollectionMissing")<{
  readonly id: string;
}> {}

interface Collections {
  readonly title: (id: string) => Effect.Effect<string, CollectionMissing>;
}
const Collections = Context.Service<Collections>("Collections");

export const CollectionHeading = (id: string) => html`<h1>${
  Effect.flatMap(Collections, (collections) => collections.title(id))
}</h1>`;

The inferred template output retains both CollectionMissing and the Collections requirement. Effect’s service contract defines how Context.Service makes that dependency explicit. Its caller decides which service to provide and how failure becomes a recovery view. The template neither hides a global client nor promises that every render succeeds. See the Renderable reference for channel inference.

Flatten producer relationships before rendering

An ordinary Fx’s emitted values are update payloads. Do not assume that emitting another Fx causes the template to recursively subscribe to a new nested producer. If the query selects a request stream, choose the intended switching/concurrency operator first and give the part the resulting output stream. Returning an Effect whose result is normalized is a different boundary from emitting arbitrary higher-order streams.

When a view displays an unexpected object, inspect the payload and its destination part. When it never appears, inspect whether its producer emits and whether required services were supplied. When a stale request wins, inspect the producer’s concurrency policy. Those are different failures; adding another template wrapper does not solve them.