Live demo islands with highlighted source: SSR-safe effect, native component-source
- signals.sx: guard effect body with (when (client?) ...) so effects
are no-op during SSR — only 2 stubs needed (effect, register-in-scope)
- sx_primitives.ml: add resource SSR stub (returns signal {loading: true}),
remove 27 unnecessary browser primitive stubs
- sx_server.ml: native component-source that looks up Component/Island
from env and pretty-prints the definition (replaces broken Python helper)
- reactive-islands/index.sx: Examples section with all 15 live demos
inline + highlighted source via component-source
- reactive-islands/demo.sx: replace 14 hardcoded highlight strings with
(component-source "~name") calls for always-current source
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2015,6 +2015,30 @@ let http_setup_page_helpers env =
|
||||
Nil)
|
||||
| _ -> raise (Eval_error "helper: expected (helper \"name\" ...args)"));
|
||||
|
||||
(* component-source — look up component/island from env, pretty-print its definition *)
|
||||
bind "component-source" (fun args ->
|
||||
match args with
|
||||
| [String name] ->
|
||||
let lookup = if String.length name > 0 && name.[0] = '~'
|
||||
then name
|
||||
else "~" ^ name in
|
||||
(try
|
||||
let comp = env_get env lookup in
|
||||
match comp with
|
||||
| Component c ->
|
||||
let params = List (List.map (fun s -> Symbol s) c.c_params) in
|
||||
let form = List [Symbol "defcomp"; Symbol ("~" ^ c.c_name);
|
||||
params; c.c_body] in
|
||||
String (pretty_print_value form)
|
||||
| Island c ->
|
||||
let params = List (List.map (fun s -> Symbol s) c.i_params) in
|
||||
let form = List [Symbol "defisland"; Symbol ("~" ^ c.i_name);
|
||||
params; c.i_body] in
|
||||
String (pretty_print_value form)
|
||||
| _ -> String (";; " ^ name ^ ": not a component")
|
||||
with _ -> String (";; component " ^ name ^ " not found"))
|
||||
| _ -> raise (Eval_error "component-source: expected (name)"));
|
||||
|
||||
(* Stub remaining demo/action helpers that need real IO *)
|
||||
let stub name = bind name (fun _args -> Nil) in
|
||||
stub "run-spec-tests";
|
||||
|
||||
@@ -686,6 +686,11 @@ let () =
|
||||
| None -> raise (Eval_error ("Store not found: " ^ name)))
|
||||
| _ -> raise (Eval_error "use-store: expected (name)"));
|
||||
register "clear-stores" (fun _args -> Hashtbl.clear store_registry; Nil);
|
||||
(* SSR stubs — effect is no-op on server (signals.sx guards with client?),
|
||||
resource returns loading state. Other browser primitives only appear
|
||||
inside effect bodies which never execute during SSR. *)
|
||||
register "effect" (fun _args -> Nil);
|
||||
register "register-in-scope" (fun _args -> Nil);
|
||||
(* resource — SSR stub: return signal with {loading: true}, client hydrates real fetch *)
|
||||
register "resource" (fun _args ->
|
||||
let state = Hashtbl.create 8 in
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
(sxbc 1 "57726b5b82c1a3cb"
|
||||
(code
|
||||
:constants ("assert-signal-value" {:upvalue-count 0 :arity 2 :constants ("deref" "assert=" "str" "Expected signal value " ", got ") :bytecode (20 0 0 16 0 48 1 17 2 20 1 0 16 2 16 1 1 3 0 16 1 1 4 0 16 2 52 2 0 4 49 3 50)} "assert-signal-has-subscribers" {:upvalue-count 0 :arity 1 :constants ("assert" ">" "len" "signal-subscribers" 0 "Expected signal to have subscribers") :bytecode (20 0 0 20 3 0 16 0 48 1 52 2 0 1 1 4 0 52 1 0 2 1 5 0 49 2 50)} "assert-signal-no-subscribers" {:upvalue-count 0 :arity 1 :constants ("assert" "=" "len" "signal-subscribers" 0 "Expected signal to have no subscribers") :bytecode (20 0 0 20 3 0 16 0 48 1 52 2 0 1 1 4 0 52 1 0 2 1 5 0 49 2 50)} "assert-signal-subscriber-count" {:upvalue-count 0 :arity 2 :constants ("len" "signal-subscribers" "assert=" "str" "Expected " " subscribers, got ") :bytecode (20 1 0 16 0 48 1 52 0 0 1 17 2 20 2 0 16 2 16 1 1 4 0 16 1 1 5 0 16 2 52 3 0 4 49 3 50)} "simulate-signal-set!" {:upvalue-count 0 :arity 2 :constants ("reset!") :bytecode (20 0 0 16 0 16 1 49 2 50)} "simulate-signal-swap!" {:upvalue-count 0 :arity 2 :constants ("swap!") :bytecode (20 0 0 16 0 16 1 49 2 50)} "assert-computed-dep-count" {:upvalue-count 0 :arity 2 :constants ("len" "signal-deps" "assert=" "str" "Expected " " deps, got ") :bytecode (20 1 0 16 0 48 1 52 0 0 1 17 2 20 2 0 16 2 16 1 1 4 0 16 1 1 5 0 16 2 52 3 0 4 49 3 50)} "assert-computed-depends-on" {:upvalue-count 0 :arity 2 :constants ("assert" "contains?" "signal-deps" "Expected computed to depend on the given signal") :bytecode (20 0 0 20 2 0 16 0 48 1 16 1 52 1 0 2 1 3 0 49 2 50)} "count-effect-runs" {:upvalue-count 0 :arity 1 :constants ("signal" 0 "effect" {:upvalue-count 1 :arity 0 :constants ("deref") :bytecode (20 0 0 18 0 49 1 50)} {:upvalue-count 2 :arity 0 :constants ("+" 1 "cek-call") :bytecode (18 0 1 1 0 52 0 0 2 19 0 5 20 2 0 18 1 2 49 2 50)}) :bytecode (20 0 0 1 1 0 48 1 17 1 20 2 0 51 3 0 1 1 48 1 5 1 1 0 17 2 20 2 0 51 4 0 1 2 1 0 48 1 17 3 16 2 50)} "make-test-signal" {:upvalue-count 0 :arity 1 :constants ("signal" "list" "effect" {:upvalue-count 2 :arity 0 :constants ("append!" "deref") :bytecode (20 0 0 18 0 20 1 0 18 1 48 1 49 2 50)} "history") :bytecode (20 0 0 16 0 48 1 17 1 52 1 0 0 17 2 20 2 0 51 3 0 1 2 1 1 48 1 5 1 0 0 16 1 1 4 0 16 2 65 2 0 50)} "assert-batch-coalesces" {:upvalue-count 0 :arity 2 :constants (0 "signal" "effect" {:upvalue-count 2 :arity 0 :constants ("deref" "+" 1) :bytecode (20 0 0 18 0 48 1 5 18 1 1 2 0 52 1 0 2 19 1 50)} "batch" "assert=" "str" "Expected " " notifications, got ") :bytecode (1 0 0 17 2 20 1 0 1 0 0 48 1 17 3 20 2 0 51 3 0 1 3 1 2 48 1 5 1 0 0 17 2 5 20 4 0 16 0 48 1 5 20 5 0 16 2 16 1 1 7 0 16 1 1 8 0 16 2 52 6 0 4 49 3 50)}) :bytecode (51 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 5 51 9 0 128 8 0 5 51 11 0 128 10 0 5 51 13 0 128 12 0 5 51 15 0 128 14 0 5 51 17 0 128 16 0 5 51 19 0 128 18 0 5 51 21 0 128 20 0 50)))
|
||||
:constants ("assert-signal-value" {:upvalue-count 0 :arity 2 :constants ("deref" "assert=" "str" "Expected signal value " ", got ") :bytecode (20 0 0 16 0 48 1 17 2 20 1 0 16 2 16 1 1 3 0 16 1 1 4 0 16 2 52 2 0 4 49 3 50)} "assert-signal-has-subscribers" {:upvalue-count 0 :arity 1 :constants ("assert" ">" "len" "signal-subscribers" 0 "Expected signal to have subscribers") :bytecode (20 0 0 20 3 0 16 0 48 1 52 2 0 1 1 4 0 52 1 0 2 1 5 0 49 2 50)} "assert-signal-no-subscribers" {:upvalue-count 0 :arity 1 :constants ("assert" "=" "len" "signal-subscribers" 0 "Expected signal to have no subscribers") :bytecode (20 0 0 20 3 0 16 0 48 1 52 2 0 1 1 4 0 52 1 0 2 1 5 0 49 2 50)} "assert-signal-subscriber-count" {:upvalue-count 0 :arity 2 :constants ("len" "signal-subscribers" "assert=" "str" "Expected " " subscribers, got ") :bytecode (20 1 0 16 0 48 1 52 0 0 1 17 2 20 2 0 16 2 16 1 1 4 0 16 1 1 5 0 16 2 52 3 0 4 49 3 50)} "simulate-signal-set!" {:upvalue-count 0 :arity 2 :constants ("reset!") :bytecode (20 0 0 16 0 16 1 49 2 50)} "simulate-signal-swap!" {:upvalue-count 0 :arity 2 :constants ("swap!") :bytecode (20 0 0 16 0 16 1 49 2 50)} "assert-computed-dep-count" {:upvalue-count 0 :arity 2 :constants ("len" "signal-deps" "assert=" "str" "Expected " " deps, got ") :bytecode (20 1 0 16 0 48 1 52 0 0 1 17 2 20 2 0 16 2 16 1 1 4 0 16 1 1 5 0 16 2 52 3 0 4 49 3 50)} "assert-computed-depends-on" {:upvalue-count 0 :arity 2 :constants ("assert" "contains?" "signal-deps" "Expected computed to depend on the given signal") :bytecode (20 0 0 20 2 0 16 0 48 1 16 1 52 1 0 2 1 3 0 49 2 50)} "count-effect-runs" {:upvalue-count 0 :arity 1 :constants ("signal" 0 "effect" {:upvalue-count 1 :arity 0 :constants ("deref") :bytecode (20 0 0 18 0 49 1 50)} {:upvalue-count 2 :arity 0 :constants ("+" 1 "cek-call") :bytecode (18 0 1 1 0 52 0 0 2 19 0 5 20 2 0 18 1 2 49 2 50)}) :bytecode (20 0 0 1 1 0 48 1 17 1 51 3 0 1 1 52 2 0 1 5 1 1 0 17 2 51 4 0 1 2 1 0 52 2 0 1 17 3 16 2 50)} "make-test-signal" {:upvalue-count 0 :arity 1 :constants ("signal" "list" "effect" {:upvalue-count 2 :arity 0 :constants ("append!" "deref") :bytecode (20 0 0 18 0 20 1 0 18 1 48 1 49 2 50)} "history") :bytecode (20 0 0 16 0 48 1 17 1 52 1 0 0 17 2 51 3 0 1 2 1 1 52 2 0 1 5 1 0 0 16 1 1 4 0 16 2 65 2 0 50)} "assert-batch-coalesces" {:upvalue-count 0 :arity 2 :constants (0 "signal" "effect" {:upvalue-count 2 :arity 0 :constants ("deref" "+" 1) :bytecode (20 0 0 18 0 48 1 5 18 1 1 2 0 52 1 0 2 19 1 50)} "batch" "assert=" "str" "Expected " " notifications, got ") :bytecode (1 0 0 17 2 20 1 0 1 0 0 48 1 17 3 51 3 0 1 3 1 2 52 2 0 1 5 1 0 0 17 2 5 20 4 0 16 0 48 1 5 20 5 0 16 2 16 1 1 7 0 16 1 1 8 0 16 2 52 6 0 4 49 3 50)}) :bytecode (51 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 5 51 9 0 128 8 0 5 51 11 0 128 10 0 5 51 13 0 128 12 0 5 51 15 0 128 14 0 5 51 17 0 128 16 0 5 51 19 0 128 18 0 5 51 21 0 128 20 0 50)))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
||||
(sxbc 1 "7e4a727b2f55684e"
|
||||
(code
|
||||
:constants ("with-marsh-scope" {:upvalue-count 0 :arity 2 :constants ("list" "with-island-scope" {:upvalue-count 1 :arity 1 :constants ("append!") :bytecode (20 0 0 18 0 16 0 49 2 50)} "dom-set-data" "sx-marsh-disposers") :bytecode (52 0 0 0 17 2 20 1 0 51 2 0 1 2 16 1 48 2 5 20 3 0 16 0 1 4 0 16 2 49 3 50)} "dispose-marsh-scope" {:upvalue-count 0 :arity 1 :constants ("dom-get-data" "sx-marsh-disposers" "for-each" {:upvalue-count 0 :arity 1 :constants ("cek-call") :bytecode (20 0 0 16 0 2 49 2 50)} "dom-set-data") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 33 24 0 51 3 0 16 1 52 2 0 2 5 20 4 0 16 0 1 1 0 2 49 3 32 1 0 2 50)} "emit-event" {:upvalue-count 0 :arity 3 :constants ("dom-dispatch") :bytecode (20 0 0 16 0 16 1 16 2 49 3 50)} "on-event" {:upvalue-count 0 :arity 3 :constants ("dom-on") :bytecode (20 0 0 16 0 16 1 16 2 49 3 50)} "bridge-event" {:upvalue-count 0 :arity 4 :constants ("effect" {:upvalue-count 4 :arity 0 :constants ("dom-on" {:upvalue-count 2 :arity 1 :constants ("event-detail" "cek-call" "list" "reset!") :bytecode (20 0 0 16 0 48 1 17 1 18 0 33 16 0 20 1 0 18 0 16 1 52 2 0 1 48 2 32 2 0 16 1 17 2 20 3 0 18 1 16 2 49 2 50)}) :bytecode (20 0 0 18 0 18 1 51 1 0 0 2 0 3 48 3 17 0 16 0 50)}) :bytecode (20 0 0 51 1 0 1 0 1 1 1 3 1 2 49 1 50)} "resource" {:upvalue-count 0 :arity 1 :constants ("signal" "dict" "loading" "data" "error" "promise-then" "cek-call" {:upvalue-count 1 :arity 1 :constants ("reset!" "dict" "loading" "data" "error") :bytecode (20 0 0 18 0 1 2 0 4 1 3 0 16 0 1 4 0 2 52 1 0 6 49 2 50)} {:upvalue-count 1 :arity 1 :constants ("reset!" "dict" "loading" "data" "error") :bytecode (20 0 0 18 0 1 2 0 4 1 3 0 2 1 4 0 16 0 52 1 0 6 49 2 50)}) :bytecode (20 0 0 1 2 0 3 1 3 0 2 1 4 0 2 52 1 0 6 48 1 17 1 20 5 0 20 6 0 16 0 2 48 2 51 7 0 1 1 51 8 0 1 1 48 3 5 16 1 50)}) :bytecode (51 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 5 51 9 0 128 8 0 5 51 11 0 128 10 0 50)))
|
||||
:constants ("with-marsh-scope" {:upvalue-count 0 :arity 2 :constants ("list" "with-island-scope" {:upvalue-count 1 :arity 1 :constants ("append!") :bytecode (20 0 0 18 0 16 0 49 2 50)} "dom-set-data" "sx-marsh-disposers") :bytecode (52 0 0 0 17 2 20 1 0 51 2 0 1 2 16 1 48 2 5 20 3 0 16 0 1 4 0 16 2 49 3 50)} "dispose-marsh-scope" {:upvalue-count 0 :arity 1 :constants ("dom-get-data" "sx-marsh-disposers" "for-each" {:upvalue-count 0 :arity 1 :constants ("cek-call") :bytecode (20 0 0 16 0 2 49 2 50)} "dom-set-data") :bytecode (20 0 0 16 0 1 1 0 48 2 17 1 16 1 33 24 0 51 3 0 16 1 52 2 0 2 5 20 4 0 16 0 1 1 0 2 49 3 32 1 0 2 50)} "emit-event" {:upvalue-count 0 :arity 3 :constants ("dom-dispatch") :bytecode (16 0 16 1 16 2 52 0 0 3 50)} "on-event" {:upvalue-count 0 :arity 3 :constants ("dom-on") :bytecode (20 0 0 16 0 16 1 16 2 49 3 50)} "bridge-event" {:upvalue-count 0 :arity 4 :constants ("effect" {:upvalue-count 4 :arity 0 :constants ("dom-on" {:upvalue-count 2 :arity 1 :constants ("event-detail" "cek-call" "list" "reset!") :bytecode (16 0 52 0 0 1 17 1 18 0 33 16 0 20 1 0 18 0 16 1 52 2 0 1 48 2 32 2 0 16 1 17 2 20 3 0 18 1 16 2 49 2 50)}) :bytecode (20 0 0 18 0 18 1 51 1 0 0 2 0 3 48 3 17 0 16 0 50)}) :bytecode (51 1 0 1 0 1 1 1 3 1 2 52 0 0 1 50)} "resource" {:upvalue-count 0 :arity 1 :constants ("signal" "dict" "loading" "data" "error" "promise-then" "cek-call" {:upvalue-count 1 :arity 1 :constants ("reset!" "dict" "loading" "data" "error") :bytecode (20 0 0 18 0 1 2 0 4 1 3 0 16 0 1 4 0 2 52 1 0 6 49 2 50)} {:upvalue-count 1 :arity 1 :constants ("reset!" "dict" "loading" "data" "error") :bytecode (20 0 0 18 0 1 2 0 4 1 3 0 2 1 4 0 16 0 52 1 0 6 49 2 50)}) :bytecode (20 0 0 1 2 0 3 1 3 0 2 1 4 0 2 52 1 0 6 48 1 17 1 20 6 0 16 0 2 48 2 51 7 0 1 1 51 8 0 1 1 52 5 0 3 5 16 1 50)}) :bytecode (51 1 0 128 0 0 5 51 3 0 128 2 0 5 51 5 0 128 4 0 5 51 7 0 128 6 0 5 51 9 0 128 8 0 5 51 11 0 128 10 0 50)))
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
((deps (list)) (disposed false) (cleanup-fn nil))
|
||||
(let
|
||||
((run-effect (fn () (when (not disposed) (when cleanup-fn (cek-call cleanup-fn nil)) (for-each (fn ((dep :as signal)) (signal-remove-sub! dep run-effect)) deps) (set! deps (list)) (let ((ctx (dict "deps" (list) "notify" run-effect))) (scope-push! "sx-reactive" ctx) (let ((result (cek-call effect-fn nil))) (scope-pop! "sx-reactive") (set! deps (get ctx "deps")) (when (callable? result) (set! cleanup-fn result))))))))
|
||||
(run-effect)
|
||||
(when (client?) (run-effect))
|
||||
(let
|
||||
((dispose-fn (fn () (set! disposed true) (when cleanup-fn (cek-call cleanup-fn nil)) (for-each (fn ((dep :as signal)) (signal-remove-sub! dep run-effect)) deps) (set! deps (list)))))
|
||||
(register-in-scope dispose-fn)
|
||||
|
||||
@@ -1,35 +1,632 @@
|
||||
(defcomp ~reactive-islands/demo/reactive-islands-demo-content () (~docs/page :title "Reactive Islands — Examples" (~docs/section :title "Live interactive islands" :id "intro" (p (strong "Every example below is a live interactive island") " — not a static code snippet. Click the buttons, type in the inputs. The signal runtime is defined in " (code "signals.sx") ", bootstrapped to JavaScript. No hand-written signal logic.") (p "Each island uses " (code "defisland") " with signals (" (code "signal") ", " (code "deref") ", " (code "reset!") ", " (code "swap!") "), derived values (" (code "computed") "), side effects (" (code "effect") "), and batch updates (" (code "batch") ").")) (~docs/section :title "Examples" :id "examples" (ol :class "space-y-1" (map (fn (item) (li (a :href (get item "href") :sx-get (get item "href") :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" :class "text-violet-600 hover:underline" (get item "label")))) reactive-examples-nav-items)))))
|
||||
(defcomp
|
||||
~reactive-islands/demo/reactive-islands-demo-content
|
||||
()
|
||||
(~docs/page
|
||||
:title "Reactive Islands — Examples"
|
||||
(~docs/section
|
||||
:title "Live interactive islands"
|
||||
:id "intro"
|
||||
(p
|
||||
(strong "Every example below is a live interactive island")
|
||||
" — not a static code snippet. Click the buttons, type in the inputs. The signal runtime is defined in "
|
||||
(code "signals.sx")
|
||||
", bootstrapped to JavaScript. No hand-written signal logic.")
|
||||
(p
|
||||
"Each island uses "
|
||||
(code "defisland")
|
||||
" with signals ("
|
||||
(code "signal")
|
||||
", "
|
||||
(code "deref")
|
||||
", "
|
||||
(code "reset!")
|
||||
", "
|
||||
(code "swap!")
|
||||
"), derived values ("
|
||||
(code "computed")
|
||||
"), side effects ("
|
||||
(code "effect")
|
||||
"), and batch updates ("
|
||||
(code "batch")
|
||||
")."))
|
||||
(~docs/section
|
||||
:title "Examples"
|
||||
:id "examples"
|
||||
(ol
|
||||
:class "space-y-1"
|
||||
(map
|
||||
(fn
|
||||
(item)
|
||||
(li
|
||||
(a
|
||||
:href (get item "href")
|
||||
:sx-get (get item "href")
|
||||
:sx-target "#main-panel"
|
||||
:sx-select "#main-panel"
|
||||
:sx-swap "outerHTML"
|
||||
:sx-push-url "true"
|
||||
:class "text-violet-600 hover:underline"
|
||||
(get item "label"))))
|
||||
reactive-examples-nav-items)))))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-counter () (~docs/page :title "Signal + Computed + Effect" (p "A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.") (~reactive-islands/index/demo-counter :initial 0) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0)))\n (doubled (computed (fn () (* 2 (deref count))))))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! count dec)) \"−\")\n (span (deref count))\n (button :on-click (fn (e) (swap! count inc)) \"+\")\n (p \"doubled: \" (deref doubled)))))" "lisp")) (p (code "(deref count)") " in a text position creates a reactive text node. When " (code "count") " changes, " (em "only that text node") " updates. " (code "doubled") " recomputes automatically. No diffing.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-counter
|
||||
()
|
||||
(~docs/page
|
||||
:title "Signal + Computed + Effect"
|
||||
(p
|
||||
"A signal holds a value. A computed derives from it. Click the buttons — the counter and doubled value update instantly, no server round-trip.")
|
||||
(~reactive-islands/index/demo-counter :initial 0)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-counter")
|
||||
"lisp"))
|
||||
(p
|
||||
(code "(deref count)")
|
||||
" in a text position creates a reactive text node. When "
|
||||
(code "count")
|
||||
" changes, "
|
||||
(em "only that text node")
|
||||
" updates. "
|
||||
(code "doubled")
|
||||
" recomputes automatically. No diffing.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-temperature () (~docs/page :title "Temperature Converter" (p "Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.") (~reactive-islands/index/demo-temperature) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/temperature ()\n (let ((celsius (signal 20)))\n (div :class \"...\"\n (button :on-click (fn (e) (swap! celsius (fn (c) (- c 5)))) \"−5\")\n (span (deref celsius))\n (button :on-click (fn (e) (swap! celsius (fn (c) (+ c 5)))) \"+5\")\n (span \"°C = \")\n (span (+ (* (deref celsius) 1.8) 32))\n (span \"°F\"))))" "lisp")) (p "The actual implementation uses " (code "computed") " for Fahrenheit: " (code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))") ". The " (code "(deref fahrenheit)") " in the span creates a reactive text node that updates when celsius changes.") (div :class "mt-6" (~reactive-islands/test-runner-placeholder) (script :type "text/sx-test" :data-for "temperature" "(defsuite \"temperature converter\" (deftest \"initial celsius is 20\" (let ((celsius (signal 20))) (assert-signal-value celsius 20))) (deftest \"computed fahrenheit = celsius * 1.8 + 32\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32))))) (assert-signal-value fahrenheit 68) (assert-computed-depends-on fahrenheit celsius))) (deftest \"+5 increments celsius\" (let ((celsius (signal 20)) (btn (mock-element \"button\"))) (mock-add-listener! btn \"click\" (fn (e) (swap! celsius (fn (c) (+ c 5))))) (simulate-click btn) (assert-signal-value celsius 25))) (deftest \"fahrenheit updates on celsius change\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32))))) (reset! celsius 0) (assert-signal-value fahrenheit 32) (reset! celsius 100) (assert-signal-value fahrenheit 212))) (deftest \"multiple clicks accumulate\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))) (btn (mock-element \"button\"))) (mock-add-listener! btn \"click\" (fn (e) (swap! celsius (fn (c) (+ c 5))))) (simulate-click btn) (simulate-click btn) (simulate-click btn) (assert-signal-value celsius 35) (assert-signal-value fahrenheit 95))))"))))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-temperature
|
||||
()
|
||||
(~docs/page
|
||||
:title "Temperature Converter"
|
||||
(p
|
||||
"Two derived values from one signal. Click to change Celsius — Fahrenheit updates reactively.")
|
||||
(~reactive-islands/index/demo-temperature)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-temperature")
|
||||
"lisp"))
|
||||
(p
|
||||
"The actual implementation uses "
|
||||
(code "computed")
|
||||
" for Fahrenheit: "
|
||||
(code "(computed (fn () (+ (* (deref celsius) 1.8) 32)))")
|
||||
". The "
|
||||
(code "(deref fahrenheit)")
|
||||
" in the span creates a reactive text node that updates when celsius changes.")
|
||||
(div
|
||||
:class "mt-6"
|
||||
(~reactive-islands/test-runner-placeholder)
|
||||
(script
|
||||
:type "text/sx-test"
|
||||
:data-for "temperature"
|
||||
"(defsuite \"temperature converter\" (deftest \"initial celsius is 20\" (let ((celsius (signal 20))) (assert-signal-value celsius 20))) (deftest \"computed fahrenheit = celsius * 1.8 + 32\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32))))) (assert-signal-value fahrenheit 68) (assert-computed-depends-on fahrenheit celsius))) (deftest \"+5 increments celsius\" (let ((celsius (signal 20)) (btn (mock-element \"button\"))) (mock-add-listener! btn \"click\" (fn (e) (swap! celsius (fn (c) (+ c 5))))) (simulate-click btn) (assert-signal-value celsius 25))) (deftest \"fahrenheit updates on celsius change\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32))))) (reset! celsius 0) (assert-signal-value fahrenheit 32) (reset! celsius 100) (assert-signal-value fahrenheit 212))) (deftest \"multiple clicks accumulate\" (let ((celsius (signal 20)) (fahrenheit (computed (fn () (+ (* (deref celsius) 1.8) 32)))) (btn (mock-element \"button\"))) (mock-add-listener! btn \"click\" (fn (e) (swap! celsius (fn (c) (+ c 5))))) (simulate-click btn) (simulate-click btn) (simulate-click btn) (assert-signal-value celsius 35) (assert-signal-value fahrenheit 95))))"))))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-stopwatch () (~docs/page :title "Effect + Cleanup: Stopwatch" (p "Effects can return cleanup functions. This stopwatch starts a " (code "set-interval") " — the cleanup clears it when the running signal toggles off.") (~reactive-islands/index/demo-stopwatch) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/stopwatch ()\n (let ((running (signal false))\n (elapsed (signal 0))\n (time-text (create-text-node \"0.0s\"))\n (btn-text (create-text-node \"Start\")))\n ;; Timer: effect creates interval, cleanup clears it\n (effect (fn ()\n (when (deref running)\n (let ((id (set-interval (fn () (swap! elapsed inc)) 100)))\n (fn () (clear-interval id))))))\n ;; Display: updates text node when elapsed changes\n (effect (fn ()\n (let ((e (deref elapsed)))\n (dom-set-text-content time-text\n (str (floor (/ e 10)) \".\" (mod e 10) \"s\")))))\n ;; Button label\n (effect (fn ()\n (dom-set-text-content btn-text\n (if (deref running) \"Stop\" \"Start\"))))\n (div :class \"...\"\n (span time-text)\n (button :on-click (fn (e) (swap! running not)) btn-text)\n (button :on-click (fn (e)\n (reset! running false) (reset! elapsed 0)) \"Reset\"))))" "lisp")) (p "Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling " (code "running") " off clears the interval. No hook rules: effects can appear anywhere, in any order.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-stopwatch
|
||||
()
|
||||
(~docs/page
|
||||
:title "Effect + Cleanup: Stopwatch"
|
||||
(p
|
||||
"Effects can return cleanup functions. This stopwatch starts a "
|
||||
(code "set-interval")
|
||||
" — the cleanup clears it when the running signal toggles off.")
|
||||
(~reactive-islands/index/demo-stopwatch)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-stopwatch")
|
||||
"lisp"))
|
||||
(p
|
||||
"Three effects, each tracking different signals. The timer effect's cleanup fires before each re-run — toggling "
|
||||
(code "running")
|
||||
" off clears the interval. No hook rules: effects can appear anywhere, in any order.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-imperative () (~docs/page :title "Imperative Pattern" (p "For complex reactivity (dynamic classes, conditional text), use the imperative pattern: " (code "create-text-node") " + " (code "effect") " + " (code "dom-set-text-content") ".") (~reactive-islands/index/demo-imperative) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/imperative ()\n (let ((count (signal 0))\n (text-node (create-text-node \"0\")))\n ;; Explicit effect: re-runs when count changes\n (effect (fn ()\n (dom-set-text-content text-node (str (deref count)))))\n (div :class \"...\"\n (span text-node)\n (button :on-click (fn (e) (swap! count inc)) \"+\"))))" "lisp")) (p "Two patterns exist: " (strong "declarative") " (" (code "(span (deref sig))") " — auto-reactive via " (code "reactive-text") ") and " (strong "imperative") " (" (code "create-text-node") " + " (code "effect") " — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-imperative
|
||||
()
|
||||
(~docs/page
|
||||
:title "Imperative Pattern"
|
||||
(p
|
||||
"For complex reactivity (dynamic classes, conditional text), use the imperative pattern: "
|
||||
(code "create-text-node")
|
||||
" + "
|
||||
(code "effect")
|
||||
" + "
|
||||
(code "dom-set-text-content")
|
||||
".")
|
||||
(~reactive-islands/index/demo-imperative)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-imperative")
|
||||
"lisp"))
|
||||
(p
|
||||
"Two patterns exist: "
|
||||
(strong "declarative")
|
||||
" ("
|
||||
(code "(span (deref sig))")
|
||||
" — auto-reactive via "
|
||||
(code "reactive-text")
|
||||
") and "
|
||||
(strong "imperative")
|
||||
" ("
|
||||
(code "create-text-node")
|
||||
" + "
|
||||
(code "effect")
|
||||
" — explicit, full control). Use declarative for simple text, imperative for dynamic classes, conditional DOM, or complex updates.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-reactive-list () (~docs/page :title "Reactive List" (p "When " (code "map") " is used with " (code "(deref signal)") " inside an island, it auto-upgrades to a reactive list. With " (code ":key") " attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.") (~reactive-islands/index/demo-reactive-list) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/reactive-list ()\n (let ((next-id (signal 1))\n (items (signal (list)))\n (add-item (fn (e)\n (batch (fn ()\n (swap! items (fn (old)\n (append old (dict \"id\" (deref next-id)\n \"text\" (str \"Item \" (deref next-id))))))\n (swap! next-id inc)))))\n (remove-item (fn (id)\n (swap! items (fn (old)\n (filter (fn (item) (not (= (get item \"id\") id))) old))))))\n (div\n (button :on-click add-item \"Add Item\")\n (span (deref (computed (fn () (len (deref items))))) \" items\")\n (ul\n (map (fn (item)\n (li :key (str (get item \"id\"))\n (span (get item \"text\"))\n (button :on-click (fn (e) (remove-item (get item \"id\"))) \"✕\")))\n (deref items))))))" "lisp")) (p (code ":key") " identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. " (code "batch") " groups the two signal writes into one update pass.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-reactive-list
|
||||
()
|
||||
(~docs/page
|
||||
:title "Reactive List"
|
||||
(p
|
||||
"When "
|
||||
(code "map")
|
||||
" is used with "
|
||||
(code "(deref signal)")
|
||||
" inside an island, it auto-upgrades to a reactive list. With "
|
||||
(code ":key")
|
||||
" attributes, existing DOM nodes are reused across updates — only additions, removals, and reorderings touch the DOM.")
|
||||
(~reactive-islands/index/demo-reactive-list)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-reactive-list")
|
||||
"lisp"))
|
||||
(p
|
||||
(code ":key")
|
||||
" identifies each list item. When items change, the reconciler matches old and new keys — reusing existing DOM nodes, inserting new ones, and removing stale ones. Without keys, the list falls back to clear-and-rerender. "
|
||||
(code "batch")
|
||||
" groups the two signal writes into one update pass.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-input-binding () (~docs/page :title "Input Binding" (p "The " (code ":bind") " attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.") (~reactive-islands/index/demo-input-binding) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/input-binding ()\n (let ((name (signal \"\"))\n (agreed (signal false)))\n (div\n (input :type \"text\" :bind name\n :placeholder \"Type your name...\")\n (span \"Hello, \" (strong (deref name)) \"!\")\n (input :type \"checkbox\" :bind agreed)\n (when (deref agreed)\n (p \"Thanks for agreeing!\")))))" "lisp")) (p (code ":bind") " detects the element type automatically — text inputs use " (code "value") " + " (code "input") " event, checkboxes use " (code "checked") " + " (code "change") " event. The effect only updates the DOM when the value actually changed, preventing cursor jump.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-input-binding
|
||||
()
|
||||
(~docs/page
|
||||
:title "Input Binding"
|
||||
(p
|
||||
"The "
|
||||
(code ":bind")
|
||||
" attribute creates a two-way link between a signal and a form element. Type in the input — the signal updates. Change the signal — the input updates. Works with text inputs, checkboxes, radios, textareas, and selects.")
|
||||
(~reactive-islands/index/demo-input-binding)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-input-binding")
|
||||
"lisp"))
|
||||
(p
|
||||
(code ":bind")
|
||||
" detects the element type automatically — text inputs use "
|
||||
(code "value")
|
||||
" + "
|
||||
(code "input")
|
||||
" event, checkboxes use "
|
||||
(code "checked")
|
||||
" + "
|
||||
(code "change")
|
||||
" event. The effect only updates the DOM when the value actually changed, preventing cursor jump.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-portal () (~docs/page :title "Portals" (p "A " (code "portal") " renders children into a DOM node " (em "outside") " the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape " (code "overflow:hidden") " or z-index stacking.") (~reactive-islands/index/demo-portal) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/portal ()\n (let ((open? (signal false)))\n (div\n (button :on-click (fn (e) (swap! open? not))\n (if (deref open?) \"Close Modal\" \"Open Modal\"))\n (portal \"#portal-root\"\n (when (deref open?)\n (div :class \"fixed inset-0 bg-black/50 ...\"\n :on-click (fn (e) (reset! open? false))\n (div :class \"bg-white rounded-lg p-6 ...\"\n :on-click (fn (e) (stop-propagation e))\n (h2 \"Portal Modal\")\n (p \"Rendered outside the island's DOM.\")\n (button :on-click (fn (e) (reset! open? false))\n \"Close\"))))))))" "lisp")) (p "The portal content lives in " (code "#portal-root") " (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the " (code "register-in-scope") " mechanism handles cleanup.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-portal
|
||||
()
|
||||
(~docs/page
|
||||
:title "Portals"
|
||||
(p
|
||||
"A "
|
||||
(code "portal")
|
||||
" renders children into a DOM node "
|
||||
(em "outside")
|
||||
" the island's subtree. Essential for modals, tooltips, and toasts — anything that must escape "
|
||||
(code "overflow:hidden")
|
||||
" or z-index stacking.")
|
||||
(~reactive-islands/index/demo-portal)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-portal")
|
||||
"lisp"))
|
||||
(p
|
||||
"The portal content lives in "
|
||||
(code "#portal-root")
|
||||
" (typically at the page body level), not inside the island. On island disposal, portal content is automatically removed from its target — the "
|
||||
(code "register-in-scope")
|
||||
" mechanism handles cleanup.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-error-boundary () (~docs/page :title "Error Boundaries" (p "When an island's rendering or effect throws, " (code "error-boundary") " catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.") (~reactive-islands/index/demo-error-boundary) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/error-boundary ()\n (let ((throw? (signal false)))\n (error-boundary\n ;; Fallback: receives (err retry-fn)\n (fn (err retry-fn)\n (div :class \"p-3 bg-red-50 border border-red-200 rounded\"\n (p :class \"text-red-700\" (error-message err))\n (button :on-click (fn (e)\n (reset! throw? false) (invoke retry-fn))\n \"Retry\")))\n ;; Children: the happy path\n (do\n (when (deref throw?) (error \"Intentional explosion!\"))\n (p \"Everything is fine.\")))))" "lisp")) (p "React equivalent: " (code "componentDidCatch") " / " (code "ErrorBoundary") ". SX's version is simpler — one form, not a class. The " (code "error-boundary") " form is a render-dom special form in " (code "adapter-dom.sx") ".")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-error-boundary
|
||||
()
|
||||
(~docs/page
|
||||
:title "Error Boundaries"
|
||||
(p
|
||||
"When an island's rendering or effect throws, "
|
||||
(code "error-boundary")
|
||||
" catches the error and renders a fallback. The fallback receives the error and a retry function. Partial effects created before the error are disposed automatically.")
|
||||
(~reactive-islands/index/demo-error-boundary)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-error-boundary")
|
||||
"lisp"))
|
||||
(p
|
||||
"React equivalent: "
|
||||
(code "componentDidCatch")
|
||||
" / "
|
||||
(code "ErrorBoundary")
|
||||
". SX's version is simpler — one form, not a class. The "
|
||||
(code "error-boundary")
|
||||
" form is a render-dom special form in "
|
||||
(code "adapter-dom.sx")
|
||||
".")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-refs () (~docs/page :title "Refs — Imperative DOM Access" (p "The " (code ":ref") " attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.") (~reactive-islands/index/demo-refs) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/refs ()\n (let ((my-ref (dict \"current\" nil))\n (msg (signal \"\")))\n (input :ref my-ref :type \"text\"\n :placeholder \"I can be focused programmatically\")\n (button :on-click (fn (e)\n (dom-focus (get my-ref \"current\")))\n \"Focus Input\")\n (button :on-click (fn (e)\n (let ((el (get my-ref \"current\")))\n (reset! msg (str \"value: \" (dom-get-prop el \"value\")))))\n \"Read Input\")\n (when (not (= (deref msg) \"\"))\n (p (deref msg)))))" "lisp")) (p "React equivalent: " (code "useRef") ". In SX, a ref is just " (code "(dict \"current\" nil)") " — no special API. The " (code ":ref") " attribute sets " (code "(dict-set! ref \"current\" el)") " when the element is created. Read it with " (code "(get ref \"current\")") ".")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-refs
|
||||
()
|
||||
(~docs/page
|
||||
:title "Refs — Imperative DOM Access"
|
||||
(p
|
||||
"The "
|
||||
(code ":ref")
|
||||
" attribute captures a DOM element handle into a dict. Use it for imperative operations: focusing, measuring, reading values.")
|
||||
(~reactive-islands/index/demo-refs)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-refs")
|
||||
"lisp"))
|
||||
(p
|
||||
"React equivalent: "
|
||||
(code "useRef")
|
||||
". In SX, a ref is just "
|
||||
(code "(dict \"current\" nil)")
|
||||
" — no special API. The "
|
||||
(code ":ref")
|
||||
" attribute sets "
|
||||
(code "(dict-set! ref \"current\" el)")
|
||||
" when the element is created. Read it with "
|
||||
(code "(get ref \"current\")")
|
||||
".")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-dynamic-class () (~docs/page :title "Dynamic Class and Style" (p "React uses " (code "className") " and " (code "style") " props with state. SX does the same — " (code "(deref signal)") " inside a " (code ":class") " or " (code ":style") " attribute creates a reactive binding. The attribute updates when the signal changes.") (~reactive-islands/index/demo-dynamic-class) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/dynamic-class ()\n (let ((danger (signal false))\n (size (signal 16)))\n (div\n (button :on-click (fn (e) (swap! danger not))\n (if (deref danger) \"Safe mode\" \"Danger mode\"))\n (button :on-click (fn (e) (swap! size (fn (s) (+ s 2))))\n \"Bigger\")\n ;; Reactive class — recomputed when danger changes\n (div :class (str \"p-3 rounded font-medium \"\n (if (deref danger)\n \"bg-red-100 text-red-800\"\n \"bg-green-100 text-green-800\"))\n ;; Reactive style — recomputed when size changes\n :style (str \"font-size:\" (deref size) \"px\")\n \"This element's class and style are reactive.\"))))" "lisp")) (p "React equivalent: " (code "className={danger ? 'red' : 'green'}") " and " (code "style={{fontSize: size}}") ". In SX the " (code "str") " + " (code "if") " + " (code "deref") " pattern handles it — no " (code "classnames") " library needed. For complex conditional classes, use a " (code "computed") " or a CSSX " (code "defcomp") " that returns a class string.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-dynamic-class
|
||||
()
|
||||
(~docs/page
|
||||
:title "Dynamic Class and Style"
|
||||
(p
|
||||
"React uses "
|
||||
(code "className")
|
||||
" and "
|
||||
(code "style")
|
||||
" props with state. SX does the same — "
|
||||
(code "(deref signal)")
|
||||
" inside a "
|
||||
(code ":class")
|
||||
" or "
|
||||
(code ":style")
|
||||
" attribute creates a reactive binding. The attribute updates when the signal changes.")
|
||||
(~reactive-islands/index/demo-dynamic-class)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-dynamic-class")
|
||||
"lisp"))
|
||||
(p
|
||||
"React equivalent: "
|
||||
(code "className={danger ? 'red' : 'green'}")
|
||||
" and "
|
||||
(code "style={{fontSize: size}}")
|
||||
". In SX the "
|
||||
(code "str")
|
||||
" + "
|
||||
(code "if")
|
||||
" + "
|
||||
(code "deref")
|
||||
" pattern handles it — no "
|
||||
(code "classnames")
|
||||
" library needed. For complex conditional classes, use a "
|
||||
(code "computed")
|
||||
" or a CSSX "
|
||||
(code "defcomp")
|
||||
" that returns a class string.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-resource () (~docs/page :title "Resource + Suspense Pattern" (p (code "resource") " wraps an async operation into a signal with " (code "loading") "/" (code "data") "/" (code "error") " states. Combined with " (code "cond") " + " (code "deref") ", this is the suspense pattern — no special form needed.") (~reactive-islands/index/demo-resource) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/resource ()\n (let ((data (resource (fn ()\n ;; Any promise-returning function\n (promise-delayed 1500\n (dict \"name\" \"Ada Lovelace\"\n \"role\" \"First Programmer\"))))))\n ;; This IS the suspense pattern:\n (let ((state (deref data)))\n (cond\n (get state \"loading\")\n (div \"Loading...\")\n (get state \"error\")\n (div \"Error: \" (get state \"error\"))\n :else\n (div (get (get state \"data\") \"name\"))))))" "lisp")) (p "React equivalent: " (code "Suspense") " + " (code "use()") " or " (code "useSWR") ". SX doesn't need a special " (code "suspense") " form because " (code "resource") " returns a signal and " (code "cond") " + " (code "deref") " creates reactive conditional rendering. When the promise resolves, the signal updates and the " (code "cond") " branch switches automatically.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-resource
|
||||
()
|
||||
(~docs/page
|
||||
:title "Resource + Suspense Pattern"
|
||||
(p
|
||||
(code "resource")
|
||||
" wraps an async operation into a signal with "
|
||||
(code "loading")
|
||||
"/"
|
||||
(code "data")
|
||||
"/"
|
||||
(code "error")
|
||||
" states. Combined with "
|
||||
(code "cond")
|
||||
" + "
|
||||
(code "deref")
|
||||
", this is the suspense pattern — no special form needed.")
|
||||
(~reactive-islands/index/demo-resource)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-resource")
|
||||
"lisp"))
|
||||
(p
|
||||
"React equivalent: "
|
||||
(code "Suspense")
|
||||
" + "
|
||||
(code "use()")
|
||||
" or "
|
||||
(code "useSWR")
|
||||
". SX doesn't need a special "
|
||||
(code "suspense")
|
||||
" form because "
|
||||
(code "resource")
|
||||
" returns a signal and "
|
||||
(code "cond")
|
||||
" + "
|
||||
(code "deref")
|
||||
" creates reactive conditional rendering. When the promise resolves, the signal updates and the "
|
||||
(code "cond")
|
||||
" branch switches automatically.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-transition () (~docs/page :title "Transition Pattern" (p "React's " (code "startTransition") " defers non-urgent updates so typing stays responsive. In SX: " (code "schedule-idle") " + " (code "batch") ". The filter runs during idle time, not blocking the input event.") (~reactive-islands/index/demo-transition) (~docs/code :src (highlight "(defisland ~reactive-islands/demo/transition ()\n (let ((query (signal \"\"))\n (all-items (list \"Signals\" \"Effects\" ...))\n (filtered (signal (list)))\n (pending (signal false)))\n (reset! filtered all-items)\n ;; Filter effect — deferred via schedule-idle\n (effect (fn ()\n (let ((q (lower (deref query))))\n (if (= q \"\")\n (do (reset! pending false)\n (reset! filtered all-items))\n (do (reset! pending true)\n (schedule-idle (fn ()\n (batch (fn ()\n (reset! filtered\n (filter (fn (item)\n (contains? (lower item) q))\n all-items))\n (reset! pending false))))))))))\n (div\n (input :bind query :placeholder \"Filter...\")\n (when (deref pending) (span \"Filtering...\"))\n (ul (map (fn (item) (li :key item item))\n (deref filtered))))))" "lisp")) (p "React equivalent: " (code "startTransition(() => setFiltered(...))") ". SX uses " (code "schedule-idle") " (" (code "requestIdleCallback") " under the hood) to defer the expensive " (code "filter") " operation, and " (code "batch") " to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-transition
|
||||
()
|
||||
(~docs/page
|
||||
:title "Transition Pattern"
|
||||
(p
|
||||
"React's "
|
||||
(code "startTransition")
|
||||
" defers non-urgent updates so typing stays responsive. In SX: "
|
||||
(code "schedule-idle")
|
||||
" + "
|
||||
(code "batch")
|
||||
". The filter runs during idle time, not blocking the input event.")
|
||||
(~reactive-islands/index/demo-transition)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-transition")
|
||||
"lisp"))
|
||||
(p
|
||||
"React equivalent: "
|
||||
(code "startTransition(() => setFiltered(...))")
|
||||
". SX uses "
|
||||
(code "schedule-idle")
|
||||
" ("
|
||||
(code "requestIdleCallback")
|
||||
" under the hood) to defer the expensive "
|
||||
(code "filter")
|
||||
" operation, and "
|
||||
(code "batch")
|
||||
" to group the result into one update. Fine-grained signals already avoid the jank that makes transitions critical in React — this pattern is for truly expensive computations.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-stores () (~docs/page :title "Shared Stores" (p "React uses " (code "Context") " or state management libraries for cross-component state. SX uses " (code "def-store") " / " (code "use-store") " — named signal containers that persist across island creation/destruction.") (~reactive-islands/index/demo-store-writer) (~reactive-islands/index/demo-store-reader) (~docs/code :src (highlight ";; Island A — creates/writes the store\n(defisland ~reactive-islands/demo/store-writer ()\n (let ((store (def-store \"theme\" (fn ()\n (dict \"color\" (signal \"violet\")\n \"dark\" (signal false))))))\n (select :bind (get store \"color\")\n (option :value \"violet\" \"Violet\")\n (option :value \"blue\" \"Blue\"))\n (input :type \"checkbox\" :bind (get store \"dark\"))))\n\n;; Island B — reads the same store, different island\n(defisland ~reactive-islands/demo/store-reader ()\n (let ((store (use-store \"theme\")))\n (div :class (str \"bg-\" (deref (get store \"color\")) \"-100\")\n \"Styled by signals from Island A\")))" "lisp")) (p "React equivalent: " (code "createContext") " + " (code "useContext") " or Redux/Zustand. Stores are simpler — just named dicts of signals at page scope. " (code "def-store") " creates once, " (code "use-store") " retrieves. Stores survive island disposal but clear on full page navigation.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-stores
|
||||
()
|
||||
(~docs/page
|
||||
:title "Shared Stores"
|
||||
(p
|
||||
"React uses "
|
||||
(code "Context")
|
||||
" or state management libraries for cross-component state. SX uses "
|
||||
(code "def-store")
|
||||
" / "
|
||||
(code "use-store")
|
||||
" — named signal containers that persist across island creation/destruction.")
|
||||
(~reactive-islands/index/demo-store-writer)
|
||||
(~reactive-islands/index/demo-store-reader)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(str
|
||||
(component-source "~reactive-islands/index/demo-store-writer")
|
||||
"\n\n"
|
||||
(component-source "~reactive-islands/index/demo-store-reader"))
|
||||
"lisp"))
|
||||
(p
|
||||
"React equivalent: "
|
||||
(code "createContext")
|
||||
" + "
|
||||
(code "useContext")
|
||||
" or Redux/Zustand. Stores are simpler — just named dicts of signals at page scope. "
|
||||
(code "def-store")
|
||||
" creates once, "
|
||||
(code "use-store")
|
||||
" retrieves. Stores survive island disposal but clear on full page navigation.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-event-bridge-demo () (~docs/page :title "Event Bridge" (p "Server-rendered content inside an island (an htmx \"lake\") can communicate with island signals via DOM custom events. Buttons with " (code "data-sx-emit") " dispatch events that island effects catch.") (~reactive-islands/index/demo-event-bridge) (~docs/code :src (highlight ";; Island listens for custom events from server-rendered content\n(defisland ~reactive-islands/demo/event-bridge ()\n (let ((messages (signal (list))))\n ;; Bridge: auto-listen for \"inbox:message\" events\n (bridge-event container \"inbox:message\" messages\n (fn (detail) (append (deref messages) (get detail \"text\"))))\n (div\n ;; Lake content (server-rendered) has data-sx-emit buttons\n (div :id \"lake\"\n :sx-get \"/my-content\"\n :sx-swap \"innerHTML\"\n :sx-trigger \"load\")\n ;; Island reads the signal reactively\n (ul (map (fn (msg) (li msg)) (deref messages))))))" "lisp")) (p "The " (code "data-sx-emit") " attribute is processed by the client engine — it adds a click handler that dispatches a CustomEvent with the JSON from " (code "data-sx-emit-detail") ". The event bubbles up to the island container where " (code "bridge-event") " catches it.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-event-bridge-demo
|
||||
()
|
||||
(~docs/page
|
||||
:title "Event Bridge"
|
||||
(p
|
||||
"Server-rendered content inside an island (an htmx \"lake\") can communicate with island signals via DOM custom events. Buttons with "
|
||||
(code "data-sx-emit")
|
||||
" dispatch events that island effects catch.")
|
||||
(~reactive-islands/index/demo-event-bridge)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
(component-source "~reactive-islands/index/demo-event-bridge")
|
||||
"lisp"))
|
||||
(p
|
||||
"The "
|
||||
(code "data-sx-emit")
|
||||
" attribute is processed by the client engine — it adds a click handler that dispatches a CustomEvent with the JSON from "
|
||||
(code "data-sx-emit-detail")
|
||||
". The event bubbles up to the island container where "
|
||||
(code "bridge-event")
|
||||
" catches it.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-defisland () (~docs/page :title "How defisland Works" (p (code "defisland") " creates a reactive component. Same calling convention as " (code "defcomp") " — keyword args, rest children — but with a reactive boundary. Inside an island, " (code "deref") " subscribes DOM nodes to signals.") (~docs/code :src (highlight ";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach" "lisp")) (p "Each " (code "deref") " call registers the enclosing DOM node as a subscriber. Signal changes update " (em "only") " the subscribed nodes — no virtual DOM, no diffing, no component re-renders.")))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-defisland
|
||||
()
|
||||
(~docs/page
|
||||
:title "How defisland Works"
|
||||
(p
|
||||
(code "defisland")
|
||||
" creates a reactive component. Same calling convention as "
|
||||
(code "defcomp")
|
||||
" — keyword args, rest children — but with a reactive boundary. Inside an island, "
|
||||
(code "deref")
|
||||
" subscribes DOM nodes to signals.")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; Definition — same syntax as defcomp\n(defisland ~reactive-islands/demo/counter (&key initial)\n (let ((count (signal (or initial 0))))\n (div\n (span (deref count)) ;; reactive text node\n (button :on-click (fn (e) (swap! count inc)) ;; event handler\n \"+\"))))\n\n;; Usage — same as any component\n(~reactive-islands/demo/counter :initial 42)\n\n;; Server-side rendering:\n;; <div data-sx-island=\"counter\" data-sx-state='{\"initial\":42}'>\n;; <span>42</span><button>+</button>\n;; </div>\n;;\n;; Client hydrates: signals + effects + event handlers attach"
|
||||
"lisp"))
|
||||
(p
|
||||
"Each "
|
||||
(code "deref")
|
||||
" call registers the enclosing DOM node as a subscriber. Signal changes update "
|
||||
(em "only")
|
||||
" the subscribed nodes — no virtual DOM, no diffing, no component re-renders.")))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-tests () (~docs/page :title "Test Suite" (p "17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).") (~docs/code :src (highlight ";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children" "lisp")) (p :class "mt-2 text-sm text-stone-500" "Run: " (code "python3 shared/sx/tests/run.py signals"))))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-tests
|
||||
()
|
||||
(~docs/page
|
||||
:title "Test Suite"
|
||||
(p
|
||||
"17 tests verify the signal runtime against the spec. All pass in the Python test runner (which uses the hand-written evaluator with native platform primitives).")
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
";; Signal basics (6 tests)\n(assert-true (signal? (signal 42)))\n(assert-equal 42 (deref (signal 42)))\n(assert-equal 5 (deref 5)) ;; non-signal passthrough\n\n;; reset! changes value\n(let ((s (signal 0)))\n (reset! s 10)\n (assert-equal 10 (deref s)))\n\n;; reset! does NOT notify when value unchanged (identical? check)\n\n;; Computed (3 tests)\n(let ((a (signal 3)) (b (signal 4))\n (sum (computed (fn () (+ (deref a) (deref b))))))\n (assert-equal 7 (deref sum))\n (reset! a 10)\n (assert-equal 14 (deref sum)))\n\n;; Effects (4 tests) — immediate run, re-run on change, dispose, cleanup\n;; Batch (1 test) — defers notifications, deduplicates subscribers\n;; defisland (3 tests) — creates island, callable, accepts children"
|
||||
"lisp"))
|
||||
(p
|
||||
:class "mt-2 text-sm text-stone-500"
|
||||
"Run: "
|
||||
(code "python3 shared/sx/tests/run.py signals"))))
|
||||
|
||||
(defcomp ~reactive-islands/demo/example-coverage () (~docs/page :title "React Feature Coverage" (p "Every React feature has an SX equivalent — most are simpler because signals are fine-grained.") (div :class "overflow-x-auto rounded border border-stone-200" (table :class "w-full text-left text-sm" (thead (tr :class "border-b border-stone-200 bg-stone-100" (th :class "px-3 py-2 font-medium text-stone-600" "React") (th :class "px-3 py-2 font-medium text-stone-600" "SX") (th :class "px-3 py-2 font-medium text-stone-600" "Demo"))) (tbody (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "useState") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(signal value)") (td :class "px-3 py-2 text-xs text-stone-500" "#1")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "useMemo") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(computed (fn () ...))") (td :class "px-3 py-2 text-xs text-stone-500" "#1, #2")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "useEffect") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(effect (fn () ...))") (td :class "px-3 py-2 text-xs text-stone-500" "#3")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "useRef") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(dict \"current\" nil) + :ref") (td :class "px-3 py-2 text-xs text-stone-500" "#9")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "useCallback") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(fn (...) ...) — no dep arrays") (td :class "px-3 py-2 text-xs text-stone-500" "N/A")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "className / style") (td :class "px-3 py-2 font-mono text-xs text-violet-700" ":class (str ...) :style (str ...)") (td :class "px-3 py-2 text-xs text-stone-500" "#10")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "Controlled inputs") (td :class "px-3 py-2 font-mono text-xs text-violet-700" ":bind signal") (td :class "px-3 py-2 text-xs text-stone-500" "#6")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "key prop") (td :class "px-3 py-2 font-mono text-xs text-violet-700" ":key value") (td :class "px-3 py-2 text-xs text-stone-500" "#5")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "createPortal") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(portal \"#target\" ...)") (td :class "px-3 py-2 text-xs text-stone-500" "#7")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "ErrorBoundary") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(error-boundary fallback ...)") (td :class "px-3 py-2 text-xs text-stone-500" "#8")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "Suspense + use()") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "(resource fn) + cond/deref") (td :class "px-3 py-2 text-xs text-stone-500" "#11")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "startTransition") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "schedule-idle + batch") (td :class "px-3 py-2 text-xs text-stone-500" "#12")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "Context / Redux") (td :class "px-3 py-2 font-mono text-xs text-violet-700" "def-store / use-store") (td :class "px-3 py-2 text-xs text-stone-500" "#13")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "Virtual DOM / diffing") (td :class "px-3 py-2 text-xs text-stone-500" "N/A — fine-grained signals update exact DOM nodes") (td :class "px-3 py-2 text-xs text-stone-500" "")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "JSX / build step") (td :class "px-3 py-2 text-xs text-stone-500" "N/A — s-expressions are the syntax") (td :class "px-3 py-2 text-xs text-stone-500" "")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "Server Components") (td :class "px-3 py-2 text-xs text-stone-500" "N/A — aser mode already expands server-side") (td :class "px-3 py-2 text-xs text-stone-500" "")) (tr :class "border-b border-stone-100" (td :class "px-3 py-2 text-stone-700" "Concurrent rendering") (td :class "px-3 py-2 text-xs text-stone-500" "N/A — fine-grained updates are inherently incremental") (td :class "px-3 py-2 text-xs text-stone-500" "")) (tr (td :class "px-3 py-2 text-stone-700" "Hooks rules") (td :class "px-3 py-2 text-xs text-stone-500" "N/A — signals are values, no ordering rules") (td :class "px-3 py-2 text-xs text-stone-500" "")))))))
|
||||
(defcomp
|
||||
~reactive-islands/demo/example-coverage
|
||||
()
|
||||
(~docs/page
|
||||
:title "React Feature Coverage"
|
||||
(p
|
||||
"Every React feature has an SX equivalent — most are simpler because signals are fine-grained.")
|
||||
(div
|
||||
:class "overflow-x-auto rounded border border-stone-200"
|
||||
(table
|
||||
:class "w-full text-left text-sm"
|
||||
(thead
|
||||
(tr
|
||||
:class "border-b border-stone-200 bg-stone-100"
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "React")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "SX")
|
||||
(th :class "px-3 py-2 font-medium text-stone-600" "Demo")))
|
||||
(tbody
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "useState")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(signal value)")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#1"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "useMemo")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(computed (fn () ...))")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#1, #2"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "useEffect")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(effect (fn () ...))")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#3"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "useRef")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(dict \"current\" nil) + :ref")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#9"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "useCallback")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(fn (...) ...) — no dep arrays")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "N/A"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "className / style")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
":class (str ...) :style (str ...)")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#10"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Controlled inputs")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
":bind signal")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#6"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "key prop")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
":key value")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#5"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "createPortal")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(portal \"#target\" ...)")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#7"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "ErrorBoundary")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(error-boundary fallback ...)")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#8"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Suspense + use()")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"(resource fn) + cond/deref")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#11"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "startTransition")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"schedule-idle + batch")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#12"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Context / Redux")
|
||||
(td
|
||||
:class "px-3 py-2 font-mono text-xs text-violet-700"
|
||||
"def-store / use-store")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "#13"))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Virtual DOM / diffing")
|
||||
(td
|
||||
:class "px-3 py-2 text-xs text-stone-500"
|
||||
"N/A — fine-grained signals update exact DOM nodes")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" ""))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "JSX / build step")
|
||||
(td
|
||||
:class "px-3 py-2 text-xs text-stone-500"
|
||||
"N/A — s-expressions are the syntax")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" ""))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Server Components")
|
||||
(td
|
||||
:class "px-3 py-2 text-xs text-stone-500"
|
||||
"N/A — aser mode already expands server-side")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" ""))
|
||||
(tr
|
||||
:class "border-b border-stone-100"
|
||||
(td :class "px-3 py-2 text-stone-700" "Concurrent rendering")
|
||||
(td
|
||||
:class "px-3 py-2 text-xs text-stone-500"
|
||||
"N/A — fine-grained updates are inherently incremental")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" ""))
|
||||
(tr
|
||||
(td :class "px-3 py-2 text-stone-700" "Hooks rules")
|
||||
(td
|
||||
:class "px-3 py-2 text-xs text-stone-500"
|
||||
"N/A — signals are values, no ordering rules")
|
||||
(td :class "px-3 py-2 text-xs text-stone-500" "")))))))
|
||||
|
||||
@@ -340,10 +340,7 @@
|
||||
(~docs/section
|
||||
:title "Resource"
|
||||
:id "demo-resource"
|
||||
(p
|
||||
"Async data fetching with "
|
||||
(code "promise-then")
|
||||
" and loading state.")
|
||||
(p "Async data fetching with loading state.")
|
||||
(~reactive-islands/index/demo-resource)
|
||||
(~docs/code
|
||||
:src (highlight
|
||||
|
||||
Reference in New Issue
Block a user