Add Client Reactivity and SX Native essays to sx docs app

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 00:11:48 +00:00
parent e085fe43b4
commit 44503a7d9b
2 changed files with 310 additions and 0 deletions

View File

@@ -70,6 +70,8 @@ ESSAYS_NAV = [
("Why S-Expressions", "/essays/why-sexps"),
("The htmx/React Hybrid", "/essays/htmx-react-hybrid"),
("On-Demand CSS", "/essays/on-demand-css"),
("Client Reactivity", "/essays/client-reactivity"),
("SX Native", "/essays/sx-native"),
]
MAIN_NAV = [

View File

@@ -1874,6 +1874,8 @@ def _essay_content_sx(slug: str) -> str:
"why-sexps": _essay_why_sexps,
"htmx-react-hybrid": _essay_htmx_react_hybrid,
"on-demand-css": _essay_on_demand_css,
"client-reactivity": _essay_client_reactivity,
"sx-native": _essay_sx_native,
}
return builders.get(slug, _essay_sx_sucks)()
@@ -2052,6 +2054,312 @@ def _essay_on_demand_css() -> str:
)
def _essay_client_reactivity() -> str:
p = '(p :class "text-stone-600"'
return (
'(~doc-page :title "Client Reactivity: The React Question"'
' (~doc-section :title "Server-driven by default" :id "server-driven"'
f' {p}'
' "sx is aligned with htmx and LiveView: the server is the source of truth. '
'Every UI state is a URL. Auth is enforced at render time. '
'There are no state synchronization bugs because there is no client state to synchronize. '
'The server renders the UI, the client swaps it in. This is the default, and it works.")'
f' {p}'
' "Most web applications do not need client-side reactivity. '
'Forms submit to the server. Navigation loads new pages. '
'Search sends a query and receives results. '
'The server-driven model handles all of this with zero client-side state management."))'
' (~doc-section :title "The dangerous path" :id "dangerous-path"'
f' {p}'
' "The progression is always the same. You add useState for a toggle. '
'Then useEffect for cleanup. Then Context to avoid prop drilling. '
'Then Suspense for async boundaries. Then a state management library '
'because Context rerenders too much. Then you have rebuilt React.")'
f' {p}'
' "Every step feels justified in isolation. '
'But each step makes the next one necessary. '
'useState creates the need for useEffect. useEffect creates the need for cleanup. '
'Cleanup creates the need for dependency arrays. '
'Dependency arrays create stale closures. '
'Stale closures create bugs that are nearly impossible to diagnose.")'
f' {p}'
' "The useEffect footgun is well-documented. '
'Memory leaks from forgotten cleanup. Race conditions from unmounted component updates. '
'Infinite render loops from dependency array mistakes. '
'These are not edge cases — they are the normal experience of React development."))'
' (~doc-section :title "What sx already has" :id "what-sx-has"'
f' {p}'
' "Before reaching for reactivity, consider what sx provides today:")'
' (div :class "overflow-x-auto mt-4"'
' (table :class "w-full text-sm text-left"'
' (thead'
' (tr :class "border-b border-stone-200"'
' (th :class "py-2 pr-4 font-semibold text-stone-700" "Capability")'
' (th :class "py-2 pr-4 font-semibold text-stone-700" "sx")'
' (th :class "py-2 font-semibold text-stone-700" "React")))'
' (tbody :class "text-stone-600"'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "Components + props")'
' (td :class "py-2 pr-4" "defcomp + &key")'
' (td :class "py-2" "JSX + props"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "Fragments / conditionals / lists")'
' (td :class "py-2 pr-4" "<>, if/when/cond, map")'
' (td :class "py-2" "<>, ternary, .map()"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "Macros")'
' (td :class "py-2 pr-4" "defmacro")'
' (td :class "py-2" "Nothing equivalent"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "OOB updates / portals")'
' (td :class "py-2 pr-4" "sx-swap-oob")'
' (td :class "py-2" "createPortal"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "DOM reconciliation")'
' (td :class "py-2 pr-4" "morphDOM (id-keyed)")'
' (td :class "py-2" "Virtual DOM diff"))'
' (tr'
' (td :class "py-2 pr-4" "Reactive client state")'
' (td :class "py-2 pr-4 italic" "None (by design)")'
' (td :class "py-2" "useState / useReducer"))))))'
' (~doc-section :title "Tier 1: Targeted escape hatches" :id "tier-1"'
f' {p}'
' "Some interactions are too small to justify a server round-trip: '
'toggling a nav menu, switching a gallery image, incrementing a quantity stepper, '
'filtering a client-side list. These need imperative DOM operations, not reactive state.")'
f' {p}'
' "Specific primitives for this tier:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (code :class "text-violet-700" "(toggle! el \\"class\\")") " — add/remove a CSS class")'
' (li (code :class "text-violet-700" "(set-attr! el \\"attr\\" value)") " — set an attribute")'
' (li (code :class "text-violet-700" "(on-event el \\"click\\" handler)") " — attach an event listener")'
' (li (code :class "text-violet-700" "(timer ms handler)") " — schedule a delayed action"))'
f' {p}'
' "These are imperative DOM operations. No reactivity graph. No subscriptions. '
'No dependency tracking. Just do the thing directly."))'
' (~doc-section :title "Tier 2: Client data primitives" :id "tier-2"'
f' {p}'
' "sxEvalAsync() returning Promises. I/O primitives — query, service, frag — '
'dispatch to /api/data/ endpoints. A two-pass async DOM renderer. '
'Pages fetch their own data client-side.")'
f' {p}'
' "This tier enables pages that render immediately with a loading skeleton, '
'then populate with data. The server sends the component structure; the client fetches data. '
'Still no reactive state — just async data loading."))'
' (~doc-section :title "Tier 3: Data-only navigation" :id "tier-3"'
f' {p}'
' "The client has page components cached in localStorage. '
'Navigation becomes a data fetch only — no sx source is transferred. '
'defpage registers a component in the page registry. '
'URL pattern matching routes to the right page component.")'
f' {p}'
' "Three data delivery modes: server-bundled (sx source + data in one response), '
'client-fetched (component cached, data fetched on mount), '
'and hybrid (server provides initial data, client refreshes).")'
f' {p}'
' "This is where sx starts to feel like a SPA — instant navigations, '
'no page reloads, cached components. But still no reactive state management."))'
' (~doc-section :title "Tier 4: Fine-grained reactivity" :id "tier-4"'
f' {p}'
' "Signals and atoms. Dependency tracking. Automatic re-renders when data changes. '
'This is the most dangerous tier because it reintroduces everything sx was designed to avoid.")'
f' {p}'
' "When it might be justified: real-time collaborative editing, '
'complex form builders with dozens of interdependent fields, '
'drag-and-drop interfaces with live previews. '
'These are genuinely hard to model as server round-trips.")'
f' {p}'
' "The escape hatch: use a Web Component wrapping a reactive library '
'(Preact, Solid, vanilla signals), mounted into the DOM via sx. '
'The reactive island is contained. It does not infect the rest of the application. '
'sx renders the page; the Web Component handles the complex interaction."))'
' (~doc-section :title "The recommendation" :id "recommendation"'
f' {p}'
' "Tier 1 now. Tier 2 next. Tier 3 when defpage coverage is high. '
'Tier 4 probably never.")'
f' {p}'
' "Each tier is independently valuable. You do not need Tier 2 to benefit from Tier 1. '
'You do not need Tier 3 to benefit from Tier 2. '
'And you almost certainly do not need Tier 4 at all.")'
f' {p}'
' "The entire point of sx is that the server is good at rendering UI. '
'Client reactivity is a last resort, not a starting point.")))'
)
def _essay_sx_native() -> str:
p = '(p :class "text-stone-600"'
return (
'(~doc-page :title "SX Native: Beyond the Browser"'
' (~doc-section :title "The thesis" :id "thesis"'
f' {p}'
' "sx.js is a ~2,300-line tree-walking interpreter with ~50 primitives. '
'The DOM is just one rendering target. '
'Swap the DOM adapter for a platform-native adapter '
'and you get React Native — but with s-expressions and a 50-primitive surface area.")'
f' {p}'
' "The interpreter does not know about HTML. It evaluates expressions, '
'calls primitives, expands macros, and hands render instructions to an adapter. '
'The adapter creates elements. Today that adapter creates DOM nodes. '
'It does not have to."))'
' (~doc-section :title "Why this isn\\\'t a WebView" :id "not-webview"'
f' {p}'
' "SX Native means the sx evaluator rendering to native UI widgets directly. '
'No DOM. No CSS. No HTML. '
'(button :on-press handler \\"Submit\\") creates a native UIButton on iOS, '
'a Material Button on Android, a GtkButton on Linux.")'
f' {p}'
' "WebView wrappers (Cordova, Capacitor, Electron) ship a browser inside your app. '
'They inherit all browser limitations: memory overhead, no native feel, '
'no platform integration. SX Native has none of these because there is no browser."))'
' (~doc-section :title "Architecture" :id "architecture"'
f' {p}'
' "The architecture splits into shared and platform-specific layers:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (strong "Shared (portable):") " Parser, evaluator, all 50+ primitives, '
'component system, macro expansion, closures, component cache")'
' (li (strong "Platform adapters:") " Web DOM, iOS UIKit/SwiftUI, Android Compose, '
'Desktop GTK/Qt, Terminal TUI, WASM"))'
f' {p}'
' "Only ~15 rendering primitives need platform-specific implementations. '
'The rest — arithmetic, string operations, list manipulation, higher-order functions, '
'control flow — are pure computation with no platform dependency."))'
' (~doc-section :title "The primitive contract" :id "primitive-contract"'
f' {p}'
' "A platform adapter implements a small interface:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (code :class "text-violet-700" "createElement(tag)") " — create a platform widget")'
' (li (code :class "text-violet-700" "createText(str)") " — create a text node")'
' (li (code :class "text-violet-700" "setAttribute(el, key, val)") " — set a property")'
' (li (code :class "text-violet-700" "appendChild(parent, child)") " — attach to tree")'
' (li (code :class "text-violet-700" "addEventListener(el, event, fn)") " — bind interaction")'
' (li (code :class "text-violet-700" "removeChild(parent, child)") " — detach from tree"))'
f' {p}'
' "Layout uses a flexbox-like model mapped to native constraint systems. '
'Styling maps a CSS property subset to native appearance APIs. '
'The mapping is lossy but covers the common cases."))'
' (~doc-section :title "What transfers, what doesn\\\'t" :id "transfers"'
f' {p}'
' "What transfers wholesale: parser, evaluator, all non-DOM primitives, '
'component system (defcomp, defmacro), closures, the component cache, '
'keyword argument handling, list/dict operations.")'
f' {p}'
' "What needs replacement: HTML tags become abstract widgets, '
'CSS becomes platform layout, SxEngine fetch/swap/history becomes native navigation, '
'innerHTML/outerHTML have no equivalent."))'
' (~doc-section :title "Component mapping" :id "component-mapping"'
f' {p}'
' "HTML elements map to platform-native widgets:")'
' (div :class "overflow-x-auto mt-4"'
' (table :class "w-full text-sm text-left"'
' (thead'
' (tr :class "border-b border-stone-200"'
' (th :class "py-2 pr-4 font-semibold text-stone-700" "HTML")'
' (th :class "py-2 font-semibold text-stone-700" "Native widget")))'
' (tbody :class "text-stone-600"'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "div")'
' (td :class "py-2" "View / Container"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "span / p")'
' (td :class "py-2" "Text / Label"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "button")'
' (td :class "py-2" "Button"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "input")'
' (td :class "py-2" "TextInput / TextField"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "img")'
' (td :class "py-2" "Image / ImageView"))'
' (tr'
' (td :class "py-2 pr-4" "ul / li")'
' (td :class "py-2" "List / ListItem"))))))'
' (~doc-section :title "Prior art" :id "prior-art"'
f' {p}'
' "React Native: JavaScript evaluated by Hermes/JSC, commands sent over a bridge '
'to native UI. Lesson: the bridge is the bottleneck. Serialization overhead, async communication, '
'layout thrashing across the boundary.")'
f' {p}'
' "Flutter: Dart compiled to native, renders via Skia/Impeller to a canvas. '
'Lesson: owning the renderer avoids platform inconsistencies but sacrifices native feel.")'
f' {p}'
' ".NET MAUI, Kotlin Multiplatform: shared logic with platform-native UI. '
'Closest to what SX Native would be.")'
f' {p}'
' "sx advantage: the evaluator is tiny (~2,300 lines), the primitive surface is minimal (~50), '
'and s-expressions are trivially portable. No bridge overhead because the evaluator runs in-process."))'
' (~doc-section :title "Language options" :id "language-options"'
f' {p}'
' "The native evaluator needs to be written in a language that compiles everywhere:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (strong "Rust") " — compiles to every target, excellent FFI, strong safety guarantees")'
' (li (strong "Zig") " — simpler, C ABI compatibility, good for embedded")'
' (li (strong "Swift") " — native on iOS, good interop with Apple platforms")'
' (li (strong "Kotlin MP") " — Android + iOS + desktop, JVM ecosystem"))'
f' {p}'
' "Recommendation: Rust evaluator core with thin Swift and Kotlin adapters '
'for iOS and Android respectively. '
'Rust compiles to WASM (replacing sx.js), native libraries (mobile/desktop), '
'and standalone binaries (CLI/server)."))'
' (~doc-section :title "Incremental path" :id "incremental-path"'
f' {p}'
' "This is not an all-or-nothing project. Each step delivers value independently:")'
' (ol :class "space-y-2 text-stone-600 mt-2 list-decimal list-inside"'
' (li "Extract platform-agnostic evaluator from sx.js — clean separation of concerns")'
' (li "Rust port of evaluator — enables WASM, edge workers, embedded")'
' (li "Terminal adapter (ratatui TUI) — simplest platform, fastest iteration cycle")'
' (li "iOS SwiftUI adapter — Rust core via swift-bridge, SwiftUI rendering")'
' (li "Android Compose adapter — Rust core via JNI, Compose rendering")'
' (li "Shared components render identically everywhere")))'
' (~doc-section :title "The federated angle" :id "federated"'
f' {p}'
' "Native sx apps are ActivityPub citizens. '
'They receive activities, evaluate component templates, and render natively. '
'A remote profile, post, or event arrives as an ActivityPub activity. '
'The native app has sx component definitions cached locally. '
'It evaluates the component with the activity data and renders platform-native UI.")'
f' {p}'
' "This is the cooperative web vision extended to native platforms. '
'Content and UI travel together as s-expressions. '
'The rendering target — browser, phone, terminal — is an implementation detail."))'
' (~doc-section :title "Realistic assessment" :id "assessment"'
f' {p}'
' "This is a multi-year project. But the architecture is sound '
'because the primitive surface is small.")'
f' {p}'
' "Immediate value: a Rust evaluator enables WASM (drop-in replacement for sx.js), '
'edge workers (Cloudflare/Deno), and embedded use cases. '
'This is worth building regardless of whether native mobile ever ships.")'
f' {p}'
' "Terminal adapter: weeks of work with ratatui. '
'Useful for CLI tools, server-side dashboards, ssh-accessible interfaces.")'
f' {p}'
' "Mobile: 6-12 months of dedicated work for a production-quality adapter. '
'The evaluator is the easy part. Platform integration — navigation, gestures, '
'accessibility, text input — is where the complexity lives.")))'
)
# ---------------------------------------------------------------------------
# Wire-format partials (for sx-get requests)
# ---------------------------------------------------------------------------