Components
Class components β reactive state and element rendering in one place.
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:
import { reactive, computed } from "elements-kit/signals";
import { Todo } from "./todo";
export class TodoStore {
@reactive() todos: Todo[] = [
new Todo("Read the docs"),
new Todo("Build something"),
new Todo("Ship it"),
];
@reactive() filter: "all" | "active" | "done" = "all";
visible = computed(() =>
this.todos.filter((t) =>
this.filter === "all" ? true : this.filter === "done" ? t.done : !t.done,
),
);
add(text: string) {
const t = text.trim();
if (t) this.todos = [...this.todos, new Todo(t)];
}
remove(id: number) {
this.todos = this.todos.filter((t) => t.id !== id);
}
}
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.
// State only β a plain signal
import { signal } from "elements-kit/signals";
const count = signal(0);
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 { , } from "elements-kit/signals";
export class {
@() = 0;
= (() => this. * 2);
() { this.++; }
() { this. = 0; }
}
export const = new ();// 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. Props arrive exactly as the caller wrote them β the runtime transforms nothing. A prop passed as a signal arrives as that signal; a prop passed as a value arrives as that value.
That makes the declared type the contract on both sides. Declare plain types when a component takes static props only β callers then cannot pass a signal:
function (: { : string }) {
return <>Hello, {.}</>;
}Declare Props<P> to accept either form on every key. Hand a prop straight to JSX, which subscribes when it is reactive, or read it with resolve:
import { } from "elements-kit/signals";
import type { } from "elements-kit/jsx-runtime";
function (
: <{ : string; ?: boolean }>,
) {
return (
<>
Hello, {.}
{() => ((.) ? "!" : ".")}
</>
);
}Props<P> is an alias for MaybeReactiveProps<P> β each key is T | Computed<T>. Use MaybeReactive<T> on individual keys when only some props should accept a signal.
Because nothing is wrapped, an omitted optional prop is plain undefined, so ?? defaults work as written:
const placeholder = resolve(props.placeholder) ?? "Ask anythingβ¦";Opt into getter props
When a body would rather read one uniform shape than branch on which form arrived, convert the props with computedProps. Every key becomes a getter β including keys the caller omitted β so reads never need resolve:
import { } from "elements-kit/signals";
import type { } from "elements-kit/jsx-runtime";
function (: <{ ?: string; ?: string }>) {
const = ();
return (
<
={.() ?? "Ask anythingβ¦"}
={.}
/>
);
}Note where the default sits. A getter is always truthy, so props.placeholder ?? "β¦" would never fall back β inside a bag the default goes on the call: props.placeholder() ?? "β¦".
Every key is a reactive source, whether the caller passed a signal or a plain value. That means a getter keeps working when you forward it to a child component β the child can call it, resolve it, or convert its own props with computedProps without wrapping it twice.
Function props
computedProps infers its shape from the argument, and a callable cannot be told apart from a getter β Computed<T> is () => T. So a prop that takes arguments (a render prop, a handler with parameters) is rejected rather than mistyped:
// β computedProps: a prop that takes arguments cannot be inferred here
const props = computedProps({ render: (item: string) => item.length });Read those off the raw props instead.
A zero-arg function prop cannot be rejected β Signal<T> and Computed<T> are zero-arg callables themselves, so banning them would ban reactive props. It types as its return value while the runtime hands back the function, so read those raw too.
Reach for computedProps when a component has several props it reads repeatedly. For one or two props, raw reads and resolve are less machinery.
See also
- Elements β JSX β DOM, prop namespaces.
- For β keyed list rendering.
- Stores β shared reactive state.
- Custom elements β components as native HTML tags.