Skip to content

Building the DOM

Compose the DOM from [<SolidComponent>] functions, [<SolidTypeComponent>] types, or both.

Large and small components

Solid does not re-render components. A component runs once, and fine-grained reactivity updates only the DOM that depends on a changed signal. So there is no performance reason to split or merge components: split them where it makes the code easier to read.

Reactivity

Solid's reactivity can surprise you if you come from React; the Solid documentation covers it in depth. The rule that matters most in Partas.Solid:

A component body runs once. A value read in the body is a snapshot. A value read inside the JSX stays live.

The two components below differ only in where they read props.active.

[<Erase>]
type SnapshotLabel() =
    inherit span()

    [<Erase>]
    member val active: bool = unbox null with get, set

    [<SolidTypeComponent>]
    member props.View =
        // Read once, when the component is created.
        let text = if props.active then "snapshot: on" else "snapshot: off"
        span () { text }

[<Erase>]
type LiveLabel() =
    inherit span()

    [<Erase>]
    member val active: bool = unbox null with get, set

    [<SolidTypeComponent>]
    member props.View =
        // Read in the JSX, so it updates.
        span () { if props.active then "live: on" else "live: off" }

[<SolidComponent>]
let ReactivityExample () =
    let active, setActive = createSignal false

    div () {
        button (onClick = fun _ -> setActive (not (active ()))) { "Toggle" }
        p () { SnapshotLabel(active = active ()) }
        p () { LiveLabel(active = active ()) }
    }
export function SnapshotLabel(props) {
    const PARTAS_OTHERS = omit(props, "active");
    const text = props.active ? "snapshot: on" : "snapshot: off";
    return <span>
        {text}
    </span>;
}

export function LiveLabel(props) {
    const PARTAS_OTHERS = omit(props, "active");
    return <span>
        {props.active ? ("live: on") : ("live: off")}
    </span>;
}

export function ReactivityExample() {
    const patternInput = createSignal(false);
    const active = patternInput[0];
    return <div>
        <button onClick={(_arg) => {
                patternInput[1](!active());
            }}>
            Toggle
        </button>
        <p>
            <SnapshotLabel active={active()} />
        </p>
        <p>
            <LiveLabel active={active()} />
        </p>
    </div>;
}

The live label follows the signal; the snapshot label keeps its first value. In the JSX tab, SnapshotLabel stores props.active ? ... : ... in a const, while LiveLabel puts the same expression in the JSX.

To keep a derived value live in the body, make it a function (let text () = ...) and call it in the JSX, or wrap it in createMemo.

Conditional rendering

An if in a children block becomes a ternary in the JSX. Show is the explicit form, with a fallback for when the condition is false.

[<SolidComponent>]
let Conditional () =
    let truthy, setTruthy = createSignal false

    div () {
        button (onClick = fun _ -> setTruthy (not (truthy ()))) { "Click me!" }
        if truthy () then
            p () { "Boo!" }
        Show(when' = not (truthy ()), fallback = p () { "It went quiet." }) {
            p () { "Do you hear that?" }
        }
    }
export function Conditional() {
    const patternInput = createSignal(false);
    const truthy = patternInput[0];
    return <div>
        <button onClick={(_arg) => {
                patternInput[1](!truthy());
            }}>
            Click me!
        </button>
        {truthy() ? (<p>
            Boo!
        </p>) : (undefined)}
        <Show when={!truthy()}
            fallback={<p>
                It went quiet.
            </p>}>
            <p>
                Do you hear that?
            </p>
        </Show>
    </div>;
}

Show.Keyed and Show.NonKeyed take a child function instead of plain children. The function receives the value, or an accessor for it. See solid-js.

Switch and Match

The first Match whose when' is true renders; Switch shows its fallback when none do.

[<SolidComponent>]
let TrafficLight () =
    let state, setState = createSignal "red"

    div () {
        button (
            onClick =
                fun _ ->
                    setState (
                        match state () with
                        | "red" -> "green"
                        | "green" -> "amber"
                        | _ -> "red"
                    )
        ) {
            "Next"
        }
        Switch(fallback = p () { "unknown" }) {
            Match(when' = (state () = "red")) { p (style = "color: #dc2626") { "Stop" } }
            Match(when' = (state () = "amber")) { p (style = "color: #d97706") { "Wait" } }
            Match(when' = (state () = "green")) { p (style = "color: #16a34a") { "Go" } }
        }
    }

Lists

For renders a list. In Solid 2 you choose how rows are kept when the list changes:

FormRows are keyed byThe child function receives
For.Keyeditem identitythe item, and an Accessor<int> for its index
For.NonKeyedpositionan Accessor for the item, and the index as an int

For.NonKeyed replaces Solid 1's Index, which no longer exists. The child is a function, so you must yield it.

[<SolidComponent>]
let Shopping () =
    let items, setItems = createSignal [| "Milk"; "Bread" |]

    div () {
        button (onClick = fun _ -> setItems (Array.append (items ()) [| "Item " + string (items().Length + 1) |])) {
            "Add"
        }
        button (onClick = fun _ -> setItems (Array.rev (items ()))) { "Reverse" }
        ol () {
            For.Keyed(each = items (), fallback = li () { "Nothing yet" }) {
                yield fun item index -> li () { item + " at " + string (index ()) }
            }
        }
    }
export function Shopping() {
    const patternInput = createSignal(["Milk", "Bread"]);
    const setItems = patternInput[1];
    const items = patternInput[0];
    return <div>
        <button onClick={(_arg) => {
                setItems(append(items(), ["Item " + int32ToString(items().length + 1)]));
            }}>
            Add
        </button>
        <button onClick={(_arg_1) => {
                setItems(reverse(items()));
            }}>
            Reverse
        </button>
        <ol>
            <KeyedFor each={items()}
                fallback={<li>
                    Nothing yet
                </li>}>
                {(item, index) => <li>
                    {(item + " at ") + int32ToString(index())}
                </li>}
            </KeyedFor>
        </ol>
    </div>;
}

Repeat renders a row per number instead of per item: Repeat(count = 3) { yield fun i -> ... }.

Async and errors

Solid 2ReplacesPurpose
LoadingSuspenseShows fallback while async values under it are pending.
ErroredErrorBoundaryShows fallback when something under it throws.
RevealSuspenseListOrders how sibling Loading boundaries reveal.

An async value is a memo whose computation returns a promise; Loading waits for it. An ErrorBoundary.Fallback gives the fallback the error and a reset function. Every third load below fails:

[<SolidComponent>]
let AsyncDemo () =
    let attempt, setAttempt = createSignal 1

    let result =
        createMemo (fun (_: string option) ->
            let n = attempt ()
            settleAfter 600 (n % 3 = 0) $"Load {n}")

    div () {
        button (onClick = fun _ -> setAttempt (attempt () + 1)) { "Load again" }
        Errored(
            fallback =
                !^(ErrorBoundary.Fallback(fun err reset ->
                    p () {
                        $"{(err () :?> exn).Message} failed. "
                        button (onClick = fun _ -> setAttempt (attempt () + 1); reset ()) { "Retry" }
                    }))
        ) {
            Loading(fallback = p () { "Loading..." }) { p () { result () } }
        }
    }
export function AsyncDemo() {
    const patternInput = createSignal(1);
    const setAttempt = patternInput[1];
    const attempt = patternInput[0];
    const result = createMemo((_arg) => {
        const n = attempt() | 0;
        return settleAfter(600, (n % 3) === 0, `Load ${n}`);
    });
    return <div>
        <button onClick={(_arg_1) => {
                setAttempt(attempt() + 1);
            }}>
            Load again
        </button>
        <Errored fallback={(err, reset) => <p>
                {concat(err().message, " failed. ")}
                <button onClick={(_arg_2) => {
                        setAttempt(attempt() + 1);
                        reset();
                    }}>
                    Retry
                </button>
            </p>}>
            <Loading fallback={<p>
                    Loading...
                </p>}>
                <p>
                    {result()}
                </p>
            </Loading>
        </Errored>
    </div>;
}

After the first load, Loading keeps the old content on screen while the next one is pending. Errored also has fallbackEle and fallbackFn setters, which set fallback without the !^. The solid-js page lists the bindings.

Edit this page