Skip to content

Components and Hooks

DomWise supports two kinds of JSX components — function components and class components — and provides a set of hooks for managing classes, prop bindings, and text nodes reactively. All of these are re-exported by @serenity-is/corelib, so application code can import them from either package.

Function Components

A function component is a plain function that receives props and returns JSX:

function Greeting(props: { name: string }) {
    return <div>Hello, {props.name}!</div>;
}

// usage
<Greeting name="World" />

Function components can use signals and hooks:

import { signal } from "@serenity-is/corelib";

function Counter() {
    const count = signal(0);
    return (
        <div>
            <span>Count: {count}</span>
            <button onClick={() => count.value++}>Increment</button>
        </div>
    );
}

Class Components

A class component extends Component and overrides render():

import { Component } from "@serenity-is/corelib";

class Greeting extends Component<{ name: string }> {
    render() {
        return <div>Hello, {this.props.name}!</div>;
    }
}

// usage
<Greeting name="World" />

Props (including children and ref) are available via this.props. The JSX factory detects class components through the static isComponent marker and forwards ref to the component instance.

Hooks

useClassList

useClassList creates a classList-like manager that can be bound to the class attribute. It mirrors the native DOMTokenList API (add, remove, toggle, contains, value, size):

import { useClassList } from "@serenity-is/corelib";

const cls = useClassList("foo");
cls.add("bar");

return <div class={cls} />;

The binding is cleaned up automatically when the element is disposed.

usePropBinding

usePropBinding creates a two-way binding that synchronizes a value to a single element attribute:

import { usePropBinding } from "@serenity-is/corelib";

const value = usePropBinding("hello");

return (
    <>
        <input value={value} />
        <button onClick={() => value("world")}>Set</button>
    </>
);

A binding may only be attached to one element and one attribute; reusing it elsewhere throws.

useText

useText creates a Text node and a setter to update its content. The node can be interpolated directly as a JSX child:

import { useText } from "@serenity-is/corelib";

const [label, setLabel] = useText("hello");

return (
    <>
        <span>{label}</span>
        <button onClick={() => setLabel("world")}>Update</button>
    </>
);

useRef

useRef is an alias for createRef:

import { useRef } from "@serenity-is/corelib";

const inputRef = useRef<HTMLInputElement>();
return <input ref={inputRef} />;

RefObject<T>.current has type T | null and starts as null. Check it before use; optional chaining is convenient when the element may not have been assigned yet:

inputRef.current?.focus();

useSignal and useUpdatableComputed

useSignal is an alias for signal, and useUpdatableComputed creates computed signals that are only recalculated when you call update(). See Signals and Reactivity.

useImperativeHandle

useImperativeHandle forwards the result of an init() function to a ref (a React-compatibility helper):

import { useImperativeHandle } from "@serenity-is/corelib";

useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));

Conditional Rendering

Show is exported by both @serenity-is/domwise and @serenity-is/corelib. It renders children when when is truthy and fallback otherwise; when can be a plain value or a signal. Function-valued branches are evaluated when shown, and previous branch content is disposed by default when the condition changes or the rendered element is disposed.

import { Show, signal } from "@serenity-is/corelib";

const loggedIn = signal(false);
return (
    <Show when={loggedIn} fallback={<Login />}>
        <Dashboard />
    </Show>
);

Set autoDispose={false} only when the branch content's lifecycle is managed elsewhere.

Components or Widgets?

Components and widgets serve different purposes:

  • Components are lightweight and ideal for presentational UI that composes with signals. They render once into real DOM nodes and have no lifecycle management of their own.
  • Widgets (from @serenity-is/corelib) manage a full lifecycle (init / destroy), associate themselves with a DOM node, and are used for editors, grids, dialogs, and other framework-integrated pieces. They are the building blocks of Serenity pages and forms.

As a rule of thumb, use components for self-contained UI fragments and widgets when you need framework integration (type registration, PropertyGrid editors, grid/dialog behavior, or lifecycle management).

See Also