Skip to content

API differences

Partas.Solid 3.0 binds Solid 2.0. Coming from Solid 1.x, also read Migrating to Solid 2.

Option objects

Where a Solid function takes an options object, the binding usually exposes the options as optional parameters, and Fable's ParamObject attribute packs them back into an object at the call site. createMemo, for example:

[<ImportMember("solid-js"); ParamObject(1)>]
static member createMemo<'T>(
    compute: 'T option -> 'T,
    ?name: string,
    ?transparent: bool,
    ?equals: EqualityFunc<'T>,
    ?unobserved: unit -> unit,
    ?``lazy``: bool,
    ?sync: bool,
    ?loadingValue: 'T
) : Accessor<'T> = jsNative
let doubled = createMemo ((fun _ -> count () * 2), name = "doubled")

compiles to

const doubled = createMemo((_arg) => count() * 2, { name: "doubled" });

Most of these functions also have an overload that takes the options as a typed object, such as MemoOptions<'T> or EffectOptions.

Special JSX attributes

Adapted from Oxpecker.Solid. Credit to Lanayx.

Attributes that do not fit a typed constructor parameter are extension methods on the element:

MethodEmits
.attr(name, value)name={value}, for any attribute
.data(name, value)data-name="value"
.bool(name, value)name={value}
.ref(element)ref={element}
.style'(obj)style={obj}, a style object
.class'(obj)class={obj}, a class object
.spread(obj){...obj}

The style and class' constructor parameters take a string. For an object, use the extension methods. .style' also takes a list of style pairs:

open Partas.Solid.Style

div().style' [ Style.backgroundColor Color.Red; "--my-var" ==> "12px" ] { "Styled" }

Solid 1's on:, prop: and use: escape hatches (.on, .prop and .use') were removed in 3.0. Solid 2 merged classList into class, so .classList is now .class'. See extension methods.

When you pass a variable to .ref, make it mutable:

open Browser.Types

[<SolidComponent>]
let RefVariable () =
    let mutable inputRef: HTMLInputElement = JS.undefined

    div () {
        input().ref (inputRef)
        button (onClick = fun _ -> inputRef.value <- "filled via ref") { "Fill" }
    }
export function RefVariable() {
    let inputRef = void 0;
    return <div>
        <input ref={inputRef} />
        <button onClick={(_arg) => {
                inputRef.value = "filled via ref";
            }}>
            Fill
        </button>
    </div>;
}

.ref also takes a callback, .ref (fun (el: HTMLDivElement) -> ...).

Stores

In Solid 2, createStore is exported from solid-js. There is no solid-js/store any more. The binding returns a Store<'T> * StoreSetter<'T>.

  • Read the current value through .Value, or let the implicit conversion to 'T do it.
  • The setter takes an updater, never a plain value. Mutate the draft and return it, or return a new value.
type Counter = { mutable count: int }

let state, setState = createStore { count = 0 }

let increment () =
    setState (fun s ->
        s.count <- s.count + 1
        s)

p () { state.Value.count }

reconcile next returns an updater too, so it reads setState (reconcile next).

The Oxpecker-style store path helper (setStore.Path.Map(...).Update(...)) was removed along with SolidStorePath. Use an updater instead.

Async data

Solid 2 removed createResource, so the old resource.current helper is gone too. An async value is now a memo whose computation returns a promise. Read it like any accessor, and put a Loading boundary above the reader:

let user = createMemo (fun (_: User option) -> fetchUser (userId ()))

Loading(fallback = p () { "Loading..." }) {
    p () { user().Name }
}

See Building the DOM.

Router

Adapted from Oxpecker.Solid. Credit to Lanayx.

The @solidjs/router bindings live in the Partas.Solid.Router namespace. Pass components to routes as a TagValue with !@:

open Partas.Solid.Router
open Partas.Solid.Web
open Browser.Dom

[<SolidComponent>]
let MyRouter () =
    Router() {
        Route(path = "/", component' = !@Home)
        Route(path = "/about", component' = !@About)
    }

render ((fun () -> MyRouter ()), document.getElementById "root") |> ignore

Add @solidjs/router to package.json. The router bindings have not yet been checked against a Solid 2 release of the router. See Solid Router.

Lazy components

lazy' binds Solid's lazy(fn, options?, moduleUrl?). importComponent was removed. Use Fable's importDynamic (from Fable.Core.JsInterop) for the import, and name the export with LazyOptions:

open Fable.Core.JsInterop

let LazyPanel: LazyComponent<HtmlElement> =
    lazy' ((fun () -> importDynamic "./Panel.fs.jsx"), LazyOptions(``export`` = "Panel"))

That compiles to

const LazyPanel = lazy(() => import("./Panel.fs.jsx"), { export: "Panel" });

Render it with Dynamic under a Loading boundary. lazyload { ... } in Partas.Solid.Experimental is a builder alternative. See Experimental.

Signal setters

A signal is a getter and setter pair. In F# their types are:

type Accessor<'T> = unit -> 'T
type Setter<'T> = 'T -> unit

A Solid setter can also take a function of the previous value. In F#, call it through Invoke:

let clicks, setClicks = createSignal 0

button (onClick = fun _ -> setClicks.Invoke(fun previous -> previous + 1)) {
    $"Clicked {clicks ()} times"
}

setter.Invoke(value) is the same as setter value. setter.InvokeAndGet(value) also returns the value that was set, as Solid 2's setter does.

Context

A context is created with createContext and read with useContext:

let ThemeContext = createContext<string> "light"

[<SolidComponent>]
let ThemedLabel () =
    let theme = useContext ThemeContext
    span () { theme }

tryUseContext returns a Result instead of throwing when no provider or default exists.

To provide a value, call the context with it and pass children. It compiles to <ThemeContext value={...}>:

[<SolidComponent>]
let ThemeApp () =
    div () {
        ThemedLabel()
        ThemeContext "dark" {
            ThemedLabel()
        }
    }
export function ThemeApp() {
    return <div>
        {untrack(ThemedLabel)}
        <ThemeContext value={"dark"}>
            {untrack(ThemedLabel)}
        </ThemeContext>
    </div>;
}
Edit this page