diff --git a/sx/content/pages.py b/sx/content/pages.py index 84c7cc9..c8bf0cb 100644 --- a/sx/content/pages.py +++ b/sx/content/pages.py @@ -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 = [ diff --git a/sx/sxc/sx_components.py b/sx/sxc/sx_components.py index d162a1c..622f885 100644 --- a/sx/sxc/sx_components.py +++ b/sx/sxc/sx_components.py @@ -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) # ---------------------------------------------------------------------------