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 2 renamed its boundaries:

Solid 1Solid 2Purpose
SuspenseLoadingShows fallback while async values under it are pending.
ErrorBoundaryErroredShows fallback when something under it throws.
SuspenseListRevealOrders how sibling Loading boundaries reveal.

createResource is gone. In Solid 2 an async value is a memo or store whose computation returns a promise, and Loading waits for it.

// fetchUser: int -> JS.Promise<User>
let user = createMemo (fun (_: User option) -> fetchUser (userId ()))

Errored(fallback = !^(p () { "Something broke." })) {
    Loading(fallback = p () { "Loading..." }) {
        p () { user().Name }
    }
}

To get the error and a reset function, pass an ErrorBoundary.Fallback:

Errored(
    fallback =
        !^(ErrorBoundary.Fallback(fun err reset ->
            div () {
                p () { (err () :?> exn).Message }
                button (onClick = fun _ -> reset ()) { "Retry" }
            }))
) {
    RiskyWidget()
}

Errored also has fallbackEle and fallbackFn setters, which set fallback without the !^: Errored(fallbackFn = ErrorBoundary.Fallback(fun err reset -> ...)).

The solid-js page lists the bindings.

Edit this page