Skip to content

Components

A component is a class with a render() method that returns an Element. It combines a store (reactive state) with element construction (JSX). The simplest possible component:

Write a component that owns its state

A typical component owns its state and produces elements from it. @reactive turns class fields into signals; JSX reads them as live bindings.

Share state across components

When state needs to be shared across components, move it into a standalone store β€” a class with @reactive fields and no render(). Components read from the store; the store holds no reference to components.

// counter-store.ts β€” state only
import {
function reactive<This extends object, Value>(source?: (self: This) => Signal<Value>): (_target: unknown, context: ClassFieldDecoratorContext<This, Value>) => (this: This, initialValue: Value) => Value

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks ―

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
,
function computed<T>(getter: (previousValue?: T) => T): () => T

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@param ― getter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // β†’ 3
a(10);
sum(); // β†’ 12 (re-evaluated lazily)

computed
} from "elements-kit/signals";
export class
class CounterStore
CounterStore
{
@
reactive<object, unknown>(source?: ((self: object) => Signal<unknown>) | undefined): (_target: unknown, context: ClassFieldDecoratorContext<object, unknown>) => (this: object, initialValue: unknown) => unknown

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks ―

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
()
CounterStore.count: number
count
= 0;
CounterStore.doubled: () => number
doubled
=
computed<number>(getter: (previousValue?: number | undefined) => number): () => number

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@param ― getter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // β†’ 3
a(10);
sum(); // β†’ 12 (re-evaluated lazily)

computed
(() => this.
CounterStore.count: number
count
* 2);
CounterStore.increment(): void
increment
() { this.
CounterStore.count: number
count
++; }
CounterStore.reset(): void
reset
() { this.
CounterStore.count: number
count
= 0; }
}
export const
const counter: CounterStore
counter
= new
constructor CounterStore(): CounterStore
CounterStore
();
// Two components, one store
class CounterDisplay {
render() {
return (
<p>{() => counter.count} Γ— 2 = {counter.doubled}</p>
);
}
}
class CounterControls {
render() {
return (
<div>
<button on:click={() => counter.increment()}>+1</button>
<button on:click={() => counter.reset()}>Reset</button>
</div>
);
}
}

The same counter instance can also drive a React component or a custom element β€” see Stores and React integration.

Rendering lists

For keyed list rendering, use the For component β€” it reconciles a reactive array into the DOM without re-rendering stable rows. See For for the full API.

Function components

A function component is a plain function that returns an Element. The JSX runtime auto-wraps the props bag, so every key on props arrives as a callable getter β€” call it to read, pass it (or wrap a call in a getter) into JSX to subscribe.

import type {
type ReactiveProps<P> = { readonly [K in keyof P]: Computed<P[K]>; } & {
readonly [RAW_PROPS]?: P;
}
ReactiveProps
} from "elements-kit/jsx-runtime";
function
function Greeting(props: ReactiveProps<{
name: string;
excited?: boolean;
}>): JSX$1.Element
Greeting
(
props: ReactiveProps<{
name: string;
excited?: boolean;
}>
props
:
type ReactiveProps<P> = { readonly [K in keyof P]: Computed<P[K]>; } & {
readonly [RAW_PROPS]?: P;
}
ReactiveProps
<{
name: string
name
: string;
excited?: boolean | undefined
excited
?: boolean }>,
) {
return (
<
p: WithJsxNamespaces<JSX.HTMLAttributes<HTMLParagraphElement>>
p
>
Hello, {
props: ReactiveProps<{
name: string;
excited?: boolean;
}>
props
.
name: Computed<string>
name
}
{() => (
props: ReactiveProps<{
name: string;
excited?: boolean;
}>
props
.
excited?: Computed<boolean | undefined> | undefined
excited
() ? "!" : ".")}
</
p: WithJsxNamespaces<JSX.HTMLAttributes<HTMLParagraphElement>>
p
>
);
}

Each key on props is a Computed<T>. Signals and computed pass through with identity preserved; static values become stable thunks. For non-JSX call sites or nested prop bags you can still call resolveProps manually.

See also

  • Elements β€” JSX β†’ DOM, prop namespaces.
  • For β€” keyed list rendering.
  • Stores β€” shared reactive state.
  • Custom elements β€” components as native HTML tags.