Visitar URL original
framework/specs/framework/12-composition-patterns.md at main · async/framework · GitHub
Skip to content

Latest commit

 

History

History
158 lines (115 loc) · 5.14 KB

12-composition-patterns.md

File metadata and controls

158 lines (115 loc) · 5.14 KB

Composition Patterns

Async composition keeps ownership explicit. Default children, component-valued props, slots, and boundaries solve different problems and should not collapse into one children escape hatch.

Pattern Map

Pattern Use When Avoid When
Default children The caller supplies content and the callee places it once. The callee must pass row, state, or option data into the content.
Component-valued props The caller chooses reusable markup and the callee renders it with explicit props. The callsite wants one-off static projection.
Slots A local child component changes after attach and old child cleanup must run. The content is static attach-time projection.
Partials and boundaries Server, route, stream, or async work owns replacement. Local component composition is enough.

Named regions, scoped templates, and source-level outlets are not current framework primitives. Current specs should keep those exact API shapes outside the current contract.

Default Children

Default children are framework-owned fragments consumed by interpolation. They are projection, not callback props.

L1 Interpret

Released:

this.render(Card, { title: "Status" }, html`<p>Ready</p>`);

No-build component hosts can pass default children with an explicit template:

<section async:component="Card">
  <template async:children>
    <p>Ready</p>
  </template>
</section>

L5 Stream

Default children stream with the boundary that contains them. Give the content its own async:boundary only when it needs independent server, route, stream, or async replacement. Independent boundaries may commit out of source order; same-boundary patches stay sequence-ordered. Streamed HTML is rescanned after insertion, so on:, signal:, class:, intersect:, and component protocol attributes activate normally.

Component-Valued Props

Component-valued props let the caller choose a component that the callee renders with explicit props. This is useful for reusable rows, icons, empty views, panels, or editors.

L1 Interpret

Released through ordinary component-valued props:

const List = component(function List({ rows, Item }) {
  return html`
    <ul>
      ${rows.map((row, index) => this.render(Item, { row, index }))}
    </ul>
  `;
});

this.render(List, { rows, Item: ProductRow });

The callee owns when each child component is rendered. The caller owns the component implementation and receives explicit props from the callee.

L5 Stream

The presenter prop chooses a component; it does not choose stream order. The callee can wrap presenter instances in async:boundary elements when each item may arrive independently. Independent presenter boundaries may commit out of source order; patches for the same presenter boundary remain sequence-ordered. Boundary swaps clean removed scopes before inserted presenter HTML is rescanned for protocol attributes.

Slots And Boundaries

Use this.slot(...) for local post-attach child replacement. Use partials and boundaries when server, route, stream, or async work owns replacement.

L1 Interpret

Released local slot:

const pane = this.slot(activePane.value, () => ({ item: selected.value }));

return html`<section on:attach="${pane.attach}"></section>`;

Released boundary target:

<section async:boundary="product"></section>

L5 Stream

this.slot(...) is local replacement owned by the current component. async:boundary is the stream target. Boundary patches can apply signal, cache, HTML, redirect, or error effects. Independent boundaries may commit out of source order; same-boundary patches are serialized by sequence number. Boundary swaps clean removed scopes before inserted HTML is scanned.

Anti-Patterns

Avoid patterns that hide ownership or turn default children into a general execution channel.

Avoid Use Instead
Function children Component-valued props.
Render prop callbacks Component-valued props with explicit props.
Authored children={...} source props Nested children or <template async:children>.
Child inspection Explicit props or component-valued props.
Clone-style child mutation Component-valued props.
Positional children arrays Explicit prop names.
Mixed slot and children APIs Slots for replacement, children for projection.

L1 Interpret

Avoid callable default children:

this.render(List, {}, function children(row) {
  return html`<li>${row.label}</li>`;
});

Use an explicit component-valued prop instead:

const Row = component(function Row({ row }) {
  return html`<li>${row.label}</li>`;
});

this.render(List, { rows, Item: Row });

L5 Stream

Out-of-order streaming does not make ambiguous composition safer. Put async:boundary on the rendered content that needs independent patches, but choose the explicit composition primitive first. The boundary controls commit order; the composition pattern controls ownership and scope. Independent boundaries may commit out of source order, same-boundary patches remain sequence-ordered, and inserted streamed HTML is rescanned for protocol attributes after each patch.