Files
rose-ash/sx/sx/essays/client-reactivity.sx
giles b0920a1121 Rename all 1,169 components to path-based names with namespace support
Component names now reflect filesystem location using / as path separator
and : as namespace separator for shared components:
  ~sx-header → ~layouts/header
  ~layout-app-body → ~shared:layout/app-body
  ~blog-admin-dashboard → ~admin/dashboard

209 files, 4,941 replacements across all services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:00:12 +00:00

3 lines
7.2 KiB
Plaintext

(defcomp ~essays/client-reactivity/essay-client-reactivity ()
(~docs/page :title "Client Reactivity: The React Question" (~docs/section :title "Server-driven by default" :id "server-driven" (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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.")) (~docs/section :title "The dangerous path" :id "dangerous-path" (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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.")) (~docs/section :title "What sx already has" :id "what-sx-has" (p :class "text-stone-600" "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")))))) (~docs/section :title "Tier 1: Targeted escape hatches" :id "tier-1" (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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")) (p :class "text-stone-600" "These are imperative DOM operations. No reactivity graph. No subscriptions. No dependency tracking. Just do the thing directly.")) (~docs/section :title "Tier 2: Client data primitives" :id "tier-2" (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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.")) (~docs/section :title "Tier 3: Data-only navigation" :id "tier-3" (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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).") (p :class "text-stone-600" "This is where sx starts to feel like a SPA — instant navigations, no page reloads, cached components. But still no reactive state management.")) (~docs/section :title "Tier 4: Fine-grained reactivity" :id "tier-4" (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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.") (p :class "text-stone-600" "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.")) (~docs/section :title "The recommendation" :id "recommendation" (p :class "text-stone-600" "Tier 1 now. Tier 2 next. Tier 3 when defpage coverage is high. Tier 4 probably never.") (p :class "text-stone-600" "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.") (p :class "text-stone-600" "The entire point of sx is that the server is good at rendering UI. Client reactivity is a last resort, not a starting point."))))