Migrating to Solid 2
Partas.Solid 3.0 targets Solid 2.0.0-rc.14; 2.x targeted Solid 1.9. Expect to touch every component that does more than render markup. Each section shows the 2.x code and what replaces it. The full 3.0 API is in Solid-JS.
Checklist
- Update the packages: Partas.Solid 3.0, Fable 5,
solid-jsand@solidjs/web2.0 (see Installation). Compile with--typedArrays false. - Replace
Fable.Browser.Domwithopen Fable.Core.TSandDom.X(DOM types). - Replace string values of closed attributes with
Attr.*cases (Attributes). - Rename
.classListto.class'; replace.on,.propand.use'(Extension methods). - Rename the control flow:
Errored,Loading,Reveal,For.Keyed/For.NonKeyed(Control flow). - Replace
splitProps/mergePropswithomit/merge(Props). - Split each
createEffectinto compute and effect functions (Effects). - Rework
createResource,batch,onMount,createSelector, store setters and the removed primitives (table). - Add
open Partas.Solid.Webforrender,Portal,Dynamicand the rest of the web runtime. - Regenerate any committed JSX snapshots.
Packages and bindings that moved
| 2.x | 3.0 |
|---|---|
Fable.Browser.Dom | Xantham.Fable.Core.TS, from its own Cloudsmith feed (Installation) |
solid-js/web | @solidjs/web, bound in Partas.Solid.Web |
solid-js/store | solid-js |
@solidjs/meta with MetaProvider | @solidjs/meta 1.0, no provider (Solid Meta) |
Router, Route, A, Navigate | @solidjs/router 2: createRouter with route definitions, plain a links (Solid Router) |
| SolidStart bindings | Removed |
DOM types
DOM types come from Fable.Core.TS.Dom. Write them as Dom.X after open Fable.Core.TS; opening Fable.Core.TS.Dom would shadow Partas's Attr module with DOM's Attr type. document, window and globalThis are bound by Partas.Solid and come in with open Fable.Core. Lookups return options:
// 2.x
open Browser.Types
let el: HTMLElement = Browser.Dom.document.getElementById "root"
// 3.0
open Fable.Core.TS
let el: Dom.HTMLElement = (document.getElementById "root").Value
Attributes and events
The HTML, ARIA and SVG attributes are generated from Solid's JSX declarations:
- A closed set of values is a
[<StringEnum>]union inAttr:type' = Attr.ButtonType.Button,role = Attr.AriaRole.Dialog. - An attribute that is only
"true"/"false"is abool. - Names follow Solid's lowercase spelling. The old camelCase spellings still compile, as
[<Obsolete>]aliases. - Event handlers take their event type:
onClick: Dom.MouseEvent -> unit. <input>'smin,maxandsteptake a number or a string.
[<SolidComponent>]
let TypedAttributes () =
let position, setPosition = createSignal "not clicked"
div () {
button (type' = Attr.ButtonType.Button, onClick = fun e -> setPosition $"{e.clientX}, {e.clientY}") {
"Click me"
}
p () { $"Last click: {position ()}" }
}
export function TypedAttributes() {
const patternInput = createSignal("not clicked");
return <div>
<button type="button"
onClick={(e) => {
patternInput[1](`${e.clientX}, ${e.clientY}`);
}}>
Click me
</button>
<p>
{concat("Last click: ", patternInput[0]())}
</p>
</div>;
}
See Oxpecker DSL, ARIA attributes and SVG elements.
Props: omit and merge
Solid 2 replaced splitProps and mergeProps with omit and merge. omit returns only the rest of the props, so there is no PARTAS_LOCAL any more: read props straight from the self identifier.
A [<SolidTypeComponent>] needs no change. The JSX tab shows the new preamble: merge for the default set in the body, omit for the props it reads.
[<Erase>]
type Chip() =
inherit span()
[<Erase>]
member val tone: string = unbox null with get, set
[<SolidTypeComponent>]
member props.View =
props.tone <- "info"
span(class' = "chip chip-" + props.tone).spread props { props.children }
[<SolidComponent>]
let ChipRow () =
div () {
Chip () { "Default tone" }
Chip (tone = "warning") { "Warning tone" }
}
export function Chip(props) {
props = merge({
tone: "info",
}, props);
const PARTAS_OTHERS = omit(props, "tone", "children");
return <span class={"chip chip-" + props.tone}
{...PARTAS_OTHERS} n$={false}>
{props.children}
</span>;
}
export function ChipRow() {
return <div>
<Chip>
Default tone
</Chip>
<Chip tone="warning">
Warning tone
</Chip>
</div>;
}
A component that reads no props gets const PARTAS_OTHERS = props and no import. If you called the functions yourself:
// 2.x
let local, others = splitProps (props, [| "class"; "size" |])
let withDefaults = mergeProps ({| size = "md" |}, props)
// 3.0
let others = omit (props, "class", "size")
let withDefaults = merge ({| size = "md" |}, props)
The self identifier no longer has to be props: member this.View compiles to function Chip(this$). Two new flags control the preamble: ComponentFlag.SkipOmit drops the omit, and ComponentFlag.SpreadProps makes .spread spread the self identifier. See Attribute flags.
The marker after a spread lost its prefix: bool:n$={false} is now n$={false}.
Extension methods
| 2.x | 3.0 |
|---|---|
.classList (obj) | .classObj (obj) |
.on (name, handler) | Removed. Use the event attribute, or an OnHandler for once, passive and capture |
.prop (name, value) | Removed |
.use' (name, value) | Removed. Solid 2 has no use: directives. Pass createDirectiveFactory to .ref |
.bool (name, value), emitting bool:name | .bool (name, value), emitting name |
.spread on #HtmlTag | .spread on #HtmlElement |
// 2.x
input().use' ("autofocus", true)
// 3.0
let autofocus = createDirectiveFactory (fun (el: Dom.HTMLInputElement) ->
el.focus ()
ignore)
input().ref autofocus
See Extension methods.
Control flow
| 2.x | 3.0 |
|---|---|
ErrorBoundary | Errored |
Suspense | Loading |
SuspenseList | Reveal |
For<'T> | For.Keyed<'T>, For.NonKeyed<'T> or For.KeyedFn<'T> |
Index<'T> | For.NonKeyed<'T> |
Show, Switch, Match | Unchanged, plus Show.Keyed, Show.NonKeyed, Match.Keyed and Match.NonKeyed |
| none | Repeat, which renders by count |
Portal, Dynamic in Partas.Solid | The same names in Partas.Solid.Web |
For.Keyed gives the row the item and an index accessor. For.NonKeyed gives it an item accessor and a plain index:
[<SolidComponent>]
let Lists () =
let names = [| "Ada"; "Grace"; "Edsger" |]
div () {
ul () {
For.Keyed(each = names) {
yield fun name index -> li () { $"{index ()}: {name}" }
}
}
ul () {
For.NonKeyed(each = names) {
yield fun name index -> li () { $"{index}: {name ()}" }
}
}
}
export function Lists() {
const names = ["Ada", "Grace", "Edsger"];
return <div>
<ul>
<KeyedFor each={names}>
{(name, index) => <li>
{`${index()}: ${name}`}
</li>}
</KeyedFor>
</ul>
<ul>
<NonKeyedFor each={names}>
{(name_1, index_1) => <li>
{`${index_1}: ${name_1()}`}
</li>}
</NonKeyedFor>
</ul>
</div>;
}
Errored's fallback gets the error as an accessor, and fallback is a union, so a function needs !^ (or set fallbackFn instead). plainFallback is gone.
// 2.x
ErrorBoundary(fallback = ErrorBoundary.Fallback(fun err reset -> div () { string err })) {
Risky ()
}
SuspenseList(revealOrder = SuspenseList.RevealOrder.Forwards) {
Suspense(fallback = Spinner ()) { Profile () }
}
// 3.0
Errored(fallback = !^(ErrorBoundary.Fallback(fun err reset -> div () { (err () :?> exn).Message }))) {
Risky ()
}
Reveal(order = Reveal.Order.Sequential) {
Loading(fallback = Spinner ()) { Profile () }
}
Removed and replaced primitives
| 2.x | 3.0 |
|---|---|
createResource | An async createMemo, under Loading. refresh re-runs it |
createComputed | A memo, or a writable derived signal (createSignal (fun () -> ...)) |
createDeferred | None |
createSelector | createProjection |
batch | flush. Writes are always batched |
startTransition, useTransition | action, with isPending |
onMount | onSettled |
on | The compute half of createEffect |
catchError | Errored |
indexArray | mapArrayUnkeyed |
produce | The store setter: mutate the draft and return it |
unwrap | snapshot |
SolidStoreSetter, SolidStorePath | StoreSetter<'T> |
importComponent | importDynamic with lazy' |
getListener | getObserver |
createResource
// 2.x
let user, manager = createResource (userId, fetchUser)
Suspense(fallback = p () { "Loading..." }) { p () { user.current.name } }
// 3.0
let user = createMemo (fun (_: User option) -> fetchUser (userId ()))
Loading(fallback = p () { "Loading..." }) { p () { user().name } }
An async memo suspends its readers until the promise settles. refresh user replaces manager.refetch (). Async memos has a live example.
Effects have two phases
An effect is a tracked compute function, which returns a value, and an untracked effect function, which gets it. Return a cleanup from the effect function instead of calling onCleanup in it:
// 2.x
createEffect (fun () ->
let id = JS.setInterval tick (delayMs ())
onCleanup (fun () -> JS.clearInterval id))
// 3.0
createEffect (
(fun (_: int option) -> delayMs ()),
fun (ms: int) ->
let id = JS.setInterval tick ms
fun () -> JS.clearInterval id
)
Effects has live examples. When the phases cannot be separated, createTrackedEffect (fun () -> ...) runs one function that tracks and acts. In Partas.Solid.Experimental, effect { } needs exactly one let! naming the tracked source, and batch { } and selector { } are gone (Experimental).
Stores
The setter takes one updater, 'T -> 'T. Path setters and produce are gone: mutate the draft and return it, or return a new value. createStore returns a Store<'T>; read it through .Value.
// 2.x
setState.Update (produce (fun s -> s.count <- s.count + 1))
// 3.0
setState (fun s ->
s.count <- s.count + 1
s)
setState (reconcile next) still works. Stores has a live example.
Curried callbacks
mapArray's two-argument callback is a Func<_, _, _>: write it as a curried lambda. In 2.x it compiled to a one-argument JS function, so Solid never passed the index.
let labels = mapArray (items, fun item index -> $"{index ()}: {item.name}")
The web runtime
Partas.Solid.Web binds @solidjs/web: render, hydrate, renderToString, renderToStream, isServer, isDev, clientOnly, httpHeader, httpStatus, useHead, Portal and Dynamic.
open Partas.Solid
open Partas.Solid.Web
render ((fun () -> App ()), (document.getElementById "root").Value)
Snapshots
Regenerated JSX shows omit/merge instead of splitProps/mergeProps, no PARTAS_LOCAL, n$ instead of bool:n$, Errored/Loading/Reveal from solid-js, KeyedFor/NonKeyedFor from Partas.Solid's compiled bindings, and web imports from @solidjs/web. See JSX output.
