Functions & Components
NX separates let bindings from component declarations. Use let for values and reusable functions. Use component when a declaration needs an emits contract or persistent state.
Runtime note: action declarations parse, lower, and behave like records. Component init returns rendered output plus an opaque host-owned state snapshot. Dispatch consumes that snapshot together with a host-ordered batch, applies the component’s own update records to its state, and returns the re-rendered output, the effect actions, and the next snapshot. See Updating state.
let Definition Mirrors Invocation
Section titled “let Definition Mirrors Invocation”type User = { name:string email:string avatarUrl:string }
let <UserCard user:User className:string = "card"/> = <div className={className}> <img src={user.avatarUrl} alt="User avatar"/> <h3>{user.name}</h3> <p>{user.email}</p> </div>
let currentUser = <User name="Ada" email="ada@example.com" avatarUrl="/ada.png"/>
// Later in the module<UserCard user={currentUser} className="featured"/>- Attributes in the definition carry type annotations.
- Default values use
=just like standard attributes. A call that leaves outclassNamegets"card". - Invocation reuses the same structure but supplies values instead of types.
- An element-style function is called only as an element.
UserCard(currentUser)is rejected: its attributes have no order a caller could rely on.
Advanced Parameters
Section titled “Advanced Parameters”type GridRow = { cells:string+ }
let <DataGrid headers:string+ rows:GridRow+ className?:string /> = <table className={className ?? "data-grid"}> <thead> <tr> for header in headers { <th>{header}</th> } </tr> </thead> <tbody> for row in rows { <tr> for cell in row.cells { <td>{cell}</td> } </tr> } </tbody> </table>- An optional prop is marked on its name (
className?:string) and reads asstring?inside the body, so it takes a fallback with??or a presence test before it is used as one value. - A default can read the parameters declared before it, and anything its own module can see.
- Iteration and conditionals in the body behave like any other expression.
Content-marked Parameters
Section titled “Content-marked Parameters”let <Dialog title:string content body:Element /> = <section> <h2>{title}</h2> {body} </section>- Prefix one property with
contentto declare where element body content binds. - The marker is contextual:
contentremains a normal identifier outside property-definition positions. - The same inline form works for element-style
let, paren-stylelet,componentprops, emitted-action payloads, and componentstate.
component Declarations
Section titled “component Declarations”action SearchSubmitted = { searchString:string}actionuses the same record-style field syntax astype Name = { ... }.- Actions remain record-compatible, so existing record construction paths keep working.
- The distinction matters only for contexts that explicitly require actions.
component <SearchBox placeholder:string emits { ValueChanged { value:string } SearchSubmitted }/> = { state { query:string = {placeholder} }
<TextInput value={query} placeholder={placeholder} />}
<SearchBox placeholder="Find docs" />- The signature keeps the element-style prop syntax.
emitscan mix inline action definitions (ValueChanged { ... }) with references to existing actions (SearchSubmitted).- Inline emitted actions become public action names such as
SearchBox.ValueChanged. - Call sites can bind handlers with
on<ActionName>and read the emitted payload through the implicitactionvalue. statedeclares persistent local fields before the rendered body expression.- State defaults are evaluated once during initialization. After that, state changes only through the component’s update record (see Updating state).
- Hosts own the serialized snapshot returned by initialization and must pass it back into later dispatch calls.
- Dispatch preserves host action order and returns effect actions in the same order handlers produce them.
UpdateandPropertyare reserved: a component cannot emit an action namedUpdateorProperty, because<Component>.Updateis its update record and<Component>.Propertyis its property union.
A call site that uses the SearchBox above binds a handler per emitted action, and a function can
build an emitted action by its public name:
action DoSearch = { search:string}
action TrackSearch = { value:string}
let makeValueChanged(value:string): SearchBox.ValueChanged = <SearchBox.ValueChanged value={value} />
<SearchBox placeholder="Find docs" onSearchSubmitted=<DoSearch search={action.searchString}/> onValueChanged=<TrackSearch value={action.value}/> />Type parameters
Section titled “Type parameters”A component that draws a collection through a template needs to say that its items and its
template agree on one item type without fixing what that type is. A type parameter does that.
It is declared with the same name:type syntax as a prop, using the keyword type as the type,
and it must come first in the signature, after extends and before every prop:
type Contact = { name:string }
external component <SkiaLayout TItem:type itemsSource?:TItem+ content children?:object+/>
let contacts:Contact* = {}
<SkiaLayout TItem=Contact itemsSource={contacts} />- Inside the signature and the body,
TItemis a type like any other:itemsSource?:TItem+and a state fieldfirst?:TItemboth work. It is distinct from every other type, including a same-named type declared outside the component, which it shadows. - A use site supplies the type by name, as a bare type name:
TItem=Contact. Any visible type qualifies — a record, a union, an alias, a primitive, or a type parameter of the enclosing component (TItem=TItemforwards it). Braced, quoted, and conditional forms are rejected. - Leave it out when nothing needs it. A use site that binds only
childrenwrites<SkiaLayout>...</SkiaLayout>and no argument. A prop typed by an unspecified parameter accepts only the empty value{}; binding anything else reports thatTItemwas not specified and shows theTItem=form to add. - A type parameter is not a prop. It carries no value, has no default, is never required, is
not a field of the runtime record, and is not a case of the component’s property union.
Once type checking has consumed a
TItem=Contactbinding, nothing below the checker sees it. - An emitted action’s payload cannot be typed by a type parameter.
emits { pick { item:TItem } }is rejected: the action is a record of its own, usable outside the component, whereTItemis not a type. - A derived component inherits its abstract base’s type parameters open, ahead of its own, and
cannot redeclare one.
<ContactList extends ItemsBase />suppliesTItemexactly as<ItemsBase />would. - Name a type parameter
Tor aT-prefixed PascalCase name:TItem,TKey,TValue. A primitive type name such asstringorobject, and the built-inElement, are rejected; any other name is allowed and shadows a same-named declared type inside the component.
Type parameters are supported on component signatures and on plain type record declarations; an
action, state group, function parameter list, function type, alias and union cannot declare one.
A record’s parameters differ from a component’s in one way that matters: they are not erased,
so <Range T=int/> and <Range T=float64/> are different types. See
Generic records.
Updating state
Section titled “Updating state”Every record, action, and component with state has a derived update record, T.Update, with
the same fields as T (a component’s state fields, never its props), every one optional. An update
record is a patch: a field it leaves out means “unchanged”, and a field set to the empty value
{} means “cleared”, which is allowed only where the field is optional in T (email?:string).
Constructing one applies no defaults and requires nothing.
type User = { name:string = "anon" email?:string }
let rename = <User.Update name="Ada" /> // only `name`; `email` is untouchedlet clearEmail = <User.Update email={} /> // `email` is clearedlet nothing = <User.Update /> // a valid, empty patchT.Updateincludes fieldsTinherits, and cannot itself be extended; there is noT.Update.Update.- An update record is an ordinary value: store it, pass it, return it, or send it to a server as the
set of changes to apply. On the wire it carries
$type: "User.Update"and only the fields that are present. - A list field is replaced whole.
Inside a component, a bare Update tag names that component’s own update record, even if the module
declares something else called Update. Outside a component, write the qualified form.
external component <Button label:string emits { Tapped { } } />
action Reset = { }
component <Counter step:int = 1 emits { Reset } /> = { state { count:int = 0 }
<Row> <Label text={count} /> <Button label="Add" onTapped=<Update count={count + step} /> /> <Button label="Reset" onTapped={<Update count=0 /> <Reset />} /> </Row>}A handler’s result is routed by type:
- The enclosing component’s own update record (
Counter.Update) patches that component’s state. - An action the enclosing component emits (
Reset, declared or inherited inemits) goes to its parent. - Anything else inside a component is a compile error: add the action to
emits, or return the component’s own update record. At the root there is no component to patch or parent to reach, so any action or update record is an effect for the host.
A handler may return one value or a list of them, mixed freely; an empty list is an error. Handler
bodies are type checked with everything their binding site can see — the component’s props and
state, enclosing let bindings and loop variables — plus action, typed by the emitted action.
State reads inside a handler are live. When dispatch runs a handler, a state field reads the value
it has at that moment, including patches applied earlier in the same batch, so tapping the counter
twice in one batch moves it by two. Props, let bindings, and loop variables keep the values they
had when the handler was created.
Dispatch runs a batch in order, applies each update record for the component to its state, collects
everything else as effects, and re-renders once at the end. Rendered output from initialization and
dispatch identifies each bound handler by a token; a host runs one by dispatching
{ $type: "ActionHandlerInvocation", token, action } with the snapshot returned alongside that
output. A token is valid only with that snapshot: every dispatch, even one whose batch is empty or
produces only effects, returns fresh tokens and retires the previous ones. Pure evaluation, which
has no snapshot to dispatch against, renders handlers without tokens. A handler that was bound
outside the component — at the root, or by a parent whose output the component renders — can still
be dispatched by its token, but it reads only what it captured and everything it returns is an
effect. The batch is atomic: if any entry fails — an unknown or stale token, a handler error, an
invalid update — dispatch fails as a whole and the snapshot the host supplied remains the current
state. Everything the host passes in — props, explicit state, and the actions in a batch, whether
or not a handler is bound for them — is checked against its declaration at every depth, so an
update record nested in a prop, a component value, an array, or an action payload meets the same
unknown-field and cleared-field rules as one the type checker saw.
Property references
Section titled “Property references”Every record, action, and component with state also has a derived property union,
T.Property: a constant union with one case per field of T (a component’s state fields, never
its props), inherited fields first. A case names a field as a value, so a sort key, a column list,
or a validation rule is typed by the fields that exist rather than by string.
type Contact = { title:string subtitle:string }
external component <Table sortBy:Contact.Property columns:Contact.Property+ />
let table = <Table sortBy=subtitle columns={ Contact.Property.title Contact.Property.subtitle } />let key: Contact.Property = {Contact.Property.title}T.Propertybehaves as any constant union: a bare case resolves at a site typedT.Property,if key is { title => "Title" subtitle => "Subtitle" }is checked for exhaustiveness, and on the wire a case is the bare field name,"subtitle".T.Propertyincludes the fieldsTinherits, cannot be extended, and has no derived declarations of its own; nor doesT.Update. Two property unions are distinct types even when their cases share names.- Inside a component, a bare
Propertynames that component’s own property union, as a bareUpdatenames its update record. Outside a component, write the qualified form.
Four intrinsic functions work on update records without naming a record type. Their names are
reserved: they resolve before anything in scope, cannot be shadowed, and a declaration named
apply, merge, diff, or changed is rejected.
| Call | Result |
|---|---|
apply(record, update) |
T: the record with each present field of the update replaced. |
merge(first, second) |
T.Update: every field present in either, the later one winning. |
diff(before, after) |
T.Update: exactly the fields whose values differ, taken from after. |
changed(update) |
T.Property*: the present fields, in declaration order. |
Every intrinsic keeps the absent-versus-empty rule: a present empty field is carried and an absent
field is never invented, so apply(u, merge(a, b)) equals apply(apply(u, a), b) and
apply(a, diff(a, b)) equals b.
external component <Button label:string emits { Tapped { } } />
component <Counter step:int = 1 /> = { state { count:int = 0 last:Counter.Property = {Property.count} }
<Row> <Label text={count} /> <Button label="Add" onTapped=<Update count={count + step} last={Property.count} /> /> </Row>}
let touched(patch:Counter.Update): Counter.Property* = {changed(patch)}Functions as values
Section titled “Functions as values”A bare identifier that names a visible function — element or paren style, declared in the same file, a same-library peer, or an import — is a value of that function’s type. Bind it to a function-typed property or value by name:
abstract external component <DrawnNode />type Contact = { name:string }type RowTemplate = <function Item:Contact Index:int />: DrawnNodeexternal component <List extends DrawnNode TItem:type ItemsSource?:TItem+ ItemTemplate?:<function Item:TItem Index:int />: DrawnNode/>external component <Label extends DrawnNode Text?:string />
let <ContactRow Item:Contact Index:int />: DrawnNode = <Label Text={"" + Index + " " + Item.name} />let <Compact Item:Contact />: DrawnNode = <Label Text={Item.name} />
let contacts = { <Contact name="Ada" /> <Contact name="Kai" /> }let full = <List TItem=Contact ItemsSource={contacts} ItemTemplate={ContactRow} />let short = <List TItem=Contact ItemsSource={contacts} ItemTemplate={Compact} />let rows: RowTemplate+ = { ContactRow Compact }A function value is a reference to its declaration and captures nothing: functions are module-level
and a body declares nothing of its own. A runtime renders one as the record
{ "$type": "Function", "module": "<module>", "name": "ContactRow" }, which a host — a list that
recycles its cells, say — calls once per item. A lexical binding of the same name shadows the
function, as it shadows any top-level name.
Invoking a function-typed value
Section titled “Invoking a function-typed value”A parameter, prop or local let whose type is a function type is invoked as an element. Arguments
bind to the type’s parameters by name, every parameter of the type is required, and the call
has the type’s result type. Continuing the example above:
component <Highlight extends DrawnNode Item:Contact Row:RowTemplate /> = { <Row Item={Item} Index=0 />}let pinned = <Highlight Item=<Contact name="Zed" /> Row={Compact} />The subset rule decides what happens at run time: the arguments reach the function by name, a
parameter the function does not declare is dropped (Compact never sees Index), and a parameter
it does declare is always present, because the type supplied it. A paren-style call on a
function-typed value, Row(item, 0), is rejected with a diagnostic showing the element form:
positions would depend on the order of parameters the value’s own declaration does not share.
Paren-style Functions
Section titled “Paren-style Functions”let clamp(value:int, low:int = 0, high:int = 100): int = { if value < low { low } else { if value > high { high } else { value } }}
let a = { clamp(150) } // 100let b = { clamp(-5, -10) } // -5let c = <clamp value=150 high=120 /> // 120: an element call skips `low`Use the paren style for small, general-purpose functions that a program calls in many places and
that read naturally with parentheses: min, floor, rgb, a formatter. Use the element style for
everything else, and always for markup. Neither is enforced; they are what the two styles are for.
- A paren-style function is called by position,
clamp(150, 0), or as an element,<clamp value=150 />, which binds arguments by name. Calling it with parentheses is the usual form. - Parameters a caller may omit — optional ones (
title?:string) and defaulted ones (low:int = 0) — come after every required parameter.let f(a?:int, b:int)is rejected. - A positional call supplies every required parameter and may stop before any of the trailing ones. To skip one in the middle, call the function as an element and name the ones you pass.
- A parameter a call leaves out takes its default, or
{}when it is optional.
Defaults belong to the function
Section titled “Defaults belong to the function”A default is evaluated by the function each time a call leaves its parameter out; it is never copied into the calling code. When a library changes a default, every existing caller gets the new one. A default sees the parameters declared before it and the declarations of the function’s own module, including private ones:
private let separator = " · "
export let join(first:string, second:string, sep:string = { separator }): string = { first + sep + second}A caller in another module writes join("a", "b") without being able to name separator. A default
cannot read a parameter declared after it, and it is checked against its parameter’s type. A
function type takes no defaults, since whoever calls a function-typed value supplies every
parameter the type declares.
See also
Section titled “See also”- Language Tour: Functions & Bindings
- Reference: Modules
- Grammar: nx-grammar.md – Functions