Skip to content

Components

In rvx, components are functions that return any type of supported content.

function Message() {
    return <h1>Hello World!</h1>;
}

<Message />
function Message() {
    return e("h1").append("Hello World!");
}

Message()

Properties

Properties are passed via the first argument as is:

function Message(props: { text: string; }) {
    return <h1>{props.text}</h1>;
}

<Message text="Hello World!" />
/**
 * @param {object} props
 * @param {string} props.text
 */
function Message(props) {
    return e("h1").append(props.text);
}

Message({ text: "Hello World!" })

In JSX, children are passed as the children property.

function Message(props: { children?: unknown; }) {
    return <h1>{props.children}</h1>;
}

<Message>Hello World!</Message>

When not using JSX, the children property is still recommended for consistency, but not a requirement.

/**
 * @param {object} props
 * @param {unknown} props.children
 */
function Message(props) {
    return e("h1").append(props.children);
}

Message({ children: "Hello World!" })

Expressions

By default, all component properties are static. To accept reactive inputs, use the Expression type.

import { Expression, sig } from "rvx";

function Counter(props: { value: Expression<number>; }) {
    return <>Current count: {props.value}</>;
}

const count = sig(42);

// Static values:
<Counter value={count.value} />

// Reactive values:
<Counter value={count} />
<Counter value={() => count.value} />
import { sig } from "./rvx.js";

/**
 * @param {object} props
 * @param {import("./rvx.js").Expression<number>} props.value
 */
function Counter(props) {
    return ["Current count: ", props.value];
}

const count = sig(42);

// Static values:
Counter({ count: 42 })
Counter({ count: count.value })

// Reactive values:
Counter({ count: count })
Counter({ count: () => count.value })

In cases where static values never make sense, you can use Reactive instead of the Expression type to disallow static values:

import { Reactive } from "rvx";

function Counter(props: { value: Reactive<number>; }) {
    return <>Current count: {props.value}</>;
}
/**
 * @param {object} props
 * @param {import("./rvx.js").Reactive<number>} props.value
 */
function Counter(props) {
    return ["Current count: ", props.value];
}

Signals

To support data flow in both directions, you can use signals as properties.

import { Signal, sig } from "rvx";

function Counter(props: { value: Signal<number>; }) {
    return <button on:click={() => { props.value.value++ }}>
        Count: {props.value}
    </button>;
}

const count = sig(0);
<Counter value={count} />
import { sig, e } from "./rvx.js";

/**
 * @param {object} props
 * @param {import("./rvx.js").Signal<number>} props.value
 */
function Counter(props) {
    return e("button").on("click", () => { props.value.value++ }).append(
        "Count: ", props.value,
    );
}

const count = sig(0);
Counter({ value: count })

Using signals for two way data flow also allows converting values in both directions in a nicely composable way.

The example below shows a basic text input and a trim function for trimming user input:

import { Signal, sig, watchUpdates } from "rvx";

function TextInput(props: { value: Signal<string>; }) {
    return <input
        type="text"
        prop:value={props.value}
        on:input={event => {
            props.value.value = (event.target as HTMLInputElement).value;
        }}
    />;
}

function trim(source: Signal<string>) {
    const input = sig(source.value);

    // Update the source signal if the input changes:
    watchUpdates(input, value => {
        source.value = value.trim();
    });

    // Update the input signal if the source changes:
    watchUpdates(source, value => {
        if (value !== input.value.trim()) {
            input.value = value;
        }
    });

    return input;
}

const text = sig("");

// This input uses the "text" signal as is:
<TextInput value={text} />

// This input uses the "trim" function to store the
// trimmed version of the input in the "text" signal:
<TextInput value={trim(text)} />

// The signal's pipe function does the same but is more
// readable when using multiple conversions:
<TextInput value={text.pipe(trim).pipe(...)} />
import { sig, watchUpdates, e } from "./rvx.js";

/**
 * @param {object} props
 * @param {import("./rvx.js").Signal<string>} props.value
 */
function TextInput(props) {
    return e("input")
        .set("type", "text")
        .prop("value", props.value)
        .on("input", event => props.value.value = event.target.value);
}

/**
 * @param {import("./rvx.js").Signal<string>} source
 */
function trim(source) {
    const input = sig(source.value);

    // Update the source signal if the input changes:
    watchUpdates(input, value => {
        source.value = value.trim();
    });

    // Update the input signal if the source changes:
    watchUpdates(source, value => {
        if (value !== input.value.trim()) {
            input.value = value;
        }
    });

    return input;
}

const text = sig("");

// This input uses the "text" signal as is:
TextInput({ value: text })

// This input uses the "trim" function to store the
// trimmed version of the input in the "text" signal:
TextInput({ value: trim(text) })

// The signal's pipe function does the same but is more
// readable when using multiple conversions:
TextInput({ value: text.pipe(trim).pipe(...) })

Forwarding special attributes

Sometimes it can be useful to forward properties to the root element of a component for allowing the user of the component to set the class, style or any other attributes.

import { ClassValue, StyleValue } from "rvx";

function Button(props: {
    class?: ClassValue;
    style?: StyleValue;
    id?: Expression<string | undefined>;
    ...
}) {
    return <button
        class={props.class}
        style={props.style}
        id={props.id}
    >...</button>;
}
import { e } from "./rvx.js";

/**
 * @param {object} props
 * @param {import("./rvx.js").ClassValue} props.class
 * @param {import("./rvx.js").StyleValue} props.style
 * @param {import("./rvx.js").Expression<string | undefined>} props.id
 */
function Button(props) {
    return e("button")
        .class(props.class)
        .style(props.style)
        .set("id", props.id)
        .append(...);
}

In case of the class and style attributes, you can use an array as value to mix properties with values from within your component:

import { ClassValue, StyleValue } from "rvx";

function Button(props: {
    class?: ClassValue;
    style?: StyleValue;
    ...
}) {
    return <button
        class={[props.class, "example"]}
        style={[props.style, { color: "red" }]}
        ...
    >...</button>;
}
import { e } from "./rvx.js";

/**
 * @param {object} props
 * @param {import("./rvx.js").ClassValue} props.class
 * @param {import("./rvx.js").StyleValue} props.style
 */
function Button(props) {
    return e("button")
        .class([props.class, "example"])
        .style([props.style, { color: "red" }])
        .append(...);
}

Lifecycle Hooks

Lifecycle hooks are supported in components:

import { teardown } from "rvx";

function Timer() {
    const elapsed = sig(0);
    const timer = setInterval(() => { elapsed.value++ }, 1000);
    teardown(() => clearInterval(timer));
    return elapsed;
}
import { teardown } from "./rvx.js";

function Timer() {
    const elapsed = sig(0);
    const timer = setInterval(() => { elapsed.value++ }, 1000);
    teardown(() => clearInterval(timer));
    return elapsed;
}