Skip to content

Reactivity

Signals, derived values and effects, and why a reactive read is a thunk.

Solidtype.Solid binds solid-js, Solid’s reactive core, at 1.9. Its names are Solid’s in snake case, so Solid’s own guide to reactivity reads across: createSignal is create_signal, createMemo is create_memo.

let count, set_count = Solid.create_signal 0 in
Solid.get count (* 0, and subscribes where it is tracked *)
Solid.set set_count 5
Solid.update set_count succ (* 6 *)

A signal is an accessor, 'a Solid.accessor, and a setter. Solid.get reads it; inside a tracking scope – a thunk in JSX, a memo, an effect – the read subscribes, so the scope reruns when the signal changes. Solid.set writes a value and Solid.update a function of the last one.

An accessor is Solid’s own function, typed uncurried, and Solid.get is inlined, so Solid.get count compiles to count(). An accessor can be handed to JavaScript that expects Solid’s, and a read costs what it costs in Solid.

A write equal to the last value, by JavaScript’s ===, is no change. ~equals:(By f) gives the equality, and ~equals:Never makes every write a change.

Solid tracks a read by when it happens: inside a tracking scope, or not at all. A read taken out of a JSX position, {count()} written as a variable read once, renders once and never updates, and nothing says so. In solidtype a dynamic attribute, child or prop is a thunk, (fun () -> ...), and anything else in a dynamic position does not compile:

3 | <div class_=(if Solidtype.Solid.get count > 2 then "hot" else "cold") />
^^^^^
Error: This constant has type string but an expression was expected of type
unit -> string
Hint: Did you forget to wrap the expression using fun () ->?

The ppx writes the thunk’s body where Solid’s compiler sees it, so the thunk costs nothing at run time: (fun () -> Jsx.int (Solid.get count)) becomes {count()}.

A function of signals is already reactive when it is read in a thunk. A memo caches it, and notifies only when its value changes:

20 °C is 68 °F
solid_derived.mlx
module S = Solidtype.Solid
module Jsx = Solidtype.Jsx
let make () =
let celsius, set_celsius = S.create_signal 20 in
let fahrenheit = S.create_memo (fun () -> (S.get celsius * 9 / 5) + 32) in
<div class_="flex items-center gap-4">
<button
class_="button button--secondary"
type_=`button
aria_label="Colder"
on_click=(fun _ -> S.update set_celsius pred)>
"-"
</button>
<span class_="tabular-nums">
(fun () -> Jsx.int (S.get celsius))
{js| °C is |js}
(fun () -> Jsx.int (S.get fahrenheit))
{js| °F|js}
</span>
<button
class_="button button--secondary"
type_=`button
aria_label="Warmer"
on_click=(fun _ -> S.update set_celsius succ)>
"+"
</button>
</div>

create_selector gives a test for a key, tracked for that key alone, so a list of a thousand items whose one is selected updates two of them when the selection moves, not all.

Solid.create_effect (fun () ->
Js.log ("count is " ^ string_of_int (Solid.get count)))

An effect runs after rendering and again whenever what it read changes. create_render_effect runs during rendering, before the DOM is connected, and create_computed at once. Solid.on deps f reads only deps, and runs f with its new and previous values:

Solid.create_effect
(Solid.on (fun () -> Solid.get count) (fun count previous -> ...))

Where Solid passes a computation its previous value, the binding is two functions: create_memo and create_memo_from ~init, create_effect and create_effect_from ~init.

Solid.batch holds every notification until it returns, and Solid.untrack reads without subscribing.

Every computation belongs to an owner and is disposed with it. Solid.on_cleanup runs when the current owner is disposed or reruns, Solid.on_mount once after the first render, and Solid.create_root makes an owner that lives until its dispose is called. Solid.get_owner and Solid.run_with_owner carry an owner into a callback, so a context or a computation made there belongs where it should.

Solid.catch_error f handler sends the errors f raises, or a computation it made raises later, to handler, as an OCaml exception: one raised in OCaml as it was raised, and a value JavaScript threw as Js.Exn.Error.

Solid solidtype
createDeferred Solid.create_deferred ?timeout_ms
createReaction Solid.create_reaction
startTransition, useTransition Solid.start_transition, Solid.use_transition
mapArray, indexArray Solid.map_array, Solid.index_array
observable, from Solid.observable, Solid.from, Solid.from_observable

Resources, a value fetched asynchronously, are on Async.