---
title: "Components and Composition in LemonadeJS"
description: "Components are plain by-value functions: embed by value or register by name, props pass by reference so states stay live, children arrive parent-scoped."
source: https://lemonadejs.com/docs/components/
---

# Components and composition

A component is a plain function, `` (props, tools) => html`...` ``, and
composition needs no machinery: there are exactly two ways to use one:

```javascript
// 1. By value: the tag IS the function; no registration, tracked by
//    imports and refactors
html`<${Card} title="Hello" total="${count}" />`

// 2. By name: register once, use anywhere (case-sensitive)
import { setComponents } from 'lemonadejs';
setComponents({ Card, Modal });
html`<Card title="Hello">...</Card>`
```

No classes, no `this`, no `self`, no hook ordering rules. A component
used twice is just called twice; there is no identity to manage.

A component by value, with children rendered in the parent scope. The live prop follows the state; the plain value does not.

<!--example-->

```js
import { html } from 'lemonadejs';

const Card = (props) => html`<div style="border:1px solid #ccc;border-radius:8px;padding:10px 14px;margin:8px 0">
    <b>${props.title}</b> <small>(${props.total} unread)</small>
    ${props.children}
</div>`;

const App = (props, { state }) => {
    const count = state(2);
    return html`<div>
        <${Card} title="Inbox" total="${count}">
            <p>${count} unread messages. This paragraph is a child, bound to the parent's state.</p>
        </${Card}>
        <${Card} title="Snapshot" total="${2}"><p>total was passed as a plain number: frozen at mount.</p></${Card}>
        <button onclick="${() => count.value++}">new message</button>
    </div>`;
};
```

## Design context

A few mechanical choices matter for composition:

- **Registration.** Components must be findable at render time, by function reference or by registered name. A misspelled or unregistered name should fail loudly, not render empty and silent.
- **Prop types.** HTML attributes are strings. Passing an object or a live state across a component boundary requires an escape mechanism; forgetting it produces `"[object Object]"` at the receiver, silently.
- **Children scope.** Content placed between component tags can bind to the parent's state or the child's, and the answer must be unambiguous and consistent.

v6 keeps one small rule set for these and makes violations loud.

## How LemonadeJS solves it

**Two forms, identical semantics.** By value (`<${Card} />`) needs no
registration and survives renames. By name (`<Card />`) reads better in
large templates: register with `setComponents({ Card })`. Names must
start with a capital letter and **match exactly**. An unknown or
misspelled name fails at mount with `LJS-104` and the registration hint,
never an empty render.

**The prop rules** (all of them):

- Literal attribute text → **string**. No guessing, no auto-casting;
  declared coercion exists, but only where it is declared: in a
  [contract](/docs/contracts/).
- `${...}` → **passed by reference, untouched**. States stay live across
  the boundary, callbacks stay callable, objects and arrays are never
  stringified.
- A bare `flag` → `true`.
- **Props are read-only** (frozen in dev). Data flows up through
  callback props, never by mutating the child. Callback names are
  lowercase (`onsave`, `onitemclick`; see [Events](/docs/events/)).

**Snapshot vs live is the caller's choice, with the same syntax as
everywhere else.** Pass the state to keep it live; pass its value to
snapshot it:

```javascript
html`<${Card} total="${count}" />`         // live: Card re-renders with count
html`<${Card} total="${count.value}" />`   // snapshot: today's number, frozen
```

One precision for components inside a *re-running* expression, a list
or a branch: each run re-delivers the plain values, and the engine
**patches** the living instance (new values flow into its prop states;
setup never re-runs) instead of rebuilding it. Frozen-at-build applies
to the template's one-time construction; entries in lists stay current.
The mechanics, and what still counts as a structural rebuild, are in
[Lists](/docs/lists/).

**Children are parent-scoped.** The nodes between the tags arrive as
`props.children`, already materialized in the *parent's* scope, so their
`${...}` bind to the parent's states:

```javascript
const Card = (props) => html`<div class="card">
    <h3>${props.title}</h3>
    ${props.children}
</div>`;

html`<${Card} title="Inbox">
    <p>${count} unread</p>     <!-- count is the PARENT's state -->
</${Card}>`
```

**Imperative surfaces use the `ref` convention.** The component calls
`props.ref?.({ open, close })` and the parent holds the api. With a
contract, the api becomes a declared, verified surface that also flows
through [React refs](/docs/react/) automatically.

Named or scoped slot patterns have no direct v6 equivalent. The answer
is a render prop: pass a function returning a view. This is a convention,
not a syntax.

## In practice

**A context menu is a stack of Modals.** The Studio contextmenu does not
implement positioning, auto-adjusting or layering: every open menu
level *is* a headerless, auto-adjusting [Modal](/docs/building-blocks/),
and the stack is an array mapped to views:

```javascript
const levelView = (lvl) => html`<${Modal} key="${lvl}" header="${false}"
    position="absolute" top="${lvl.top}" left="${lvl.left}" focus="${false}" autoadjust>
    <ul class="lm-contextmenu-list" role="menu">${lvl.options.map(itemView)}</ul>
</${Modal}>`;

return html`<div class="lm-contextmenu">
    ${() => levels.value.map((lvl, li) => levelView(lvl, li))}
</div>`;
```

The list re-runs every time a submenu opens or closes, and the surviving
Modal entries are **patched in place**, not rebuilt. `key="${lvl}"`
keys each level by its own object, and the engine flows changed values
into the living instances ([Lists](/docs/lists/) is the full story). This
matters here for a visible reason: a rebuilt Modal would re-run
auto-adjust and move the parent menu when a submenu opens. An earlier
version of this component worked around the rebuild with a `WeakMap`
view cache; live patching made the cache unnecessary and the workaround
was deleted from the source.

**Any block can live in a datagrid cell.** The datagrid's column
`render` is a render prop returning a string or an `html` view. From
the shipped demo, a working Switch inside a cell of a 100,000-row grid:

```javascript
{
    name: 'active', title: 'Active', align: 'center',
    render: (value, row) =>
        html`<${Switch} checked="${!!value}" size="small"
            onchange="${(on) => { row.active = on; rows.touch(); }}" />`,
}
```

The cell view composes a full contract component; the `onchange`
callback mutates the caller's own row and notifies. The
[state model](/docs/state/) and the composition model are the same model.

## Reference

```javascript
import { setComponents } from 'lemonadejs';
setComponents({ Card, Modal });   // by name: capitalized, exact match, LJS-104 if unknown
html`<${Card} ... />`             // by value: no registration, same props handling

// prop rules
title="Hello"             // → string
total="${count}"          // → by reference: state stays LIVE
total="${count.value}"    // → snapshot of today's value
disabled                  // → true
onsave="${(v) => ...}"    // → callback prop (lowercase, LJS-305)

// children and api
<${Card}>...</${Card}>            // → props.children, bound to the PARENT scope
props.ref?.({ open, close })      // expose an imperative api to the caller
```

- Props are frozen in dev; flow data up with callbacks.
- Components are by-value plain functions: no `this`, no instance
  registry, nothing to clean up that [unmount](/docs/destroy/) does not
  already own.
- When a component becomes a product, published, consumed by other
  stacks or other agents, add a [contract](/docs/contracts/).