From 4ba63bda17304a9df84f37a7fb2f5e37961b6bab Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 3 Mar 2026 17:48:35 +0000 Subject: [PATCH] Add server-driven architecture principle and React feature analysis Documents why sx stays server-driven by default, maps React features to sx equivalents, and defines targeted escape hatches for the few interactions that genuinely need client-side state. Co-Authored-By: Claude Opus 4.6 --- docs/isomorphic-sx-plan.md | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/isomorphic-sx-plan.md b/docs/isomorphic-sx-plan.md index 9e48846..9ebdaf8 100644 --- a/docs/isomorphic-sx-plan.md +++ b/docs/isomorphic-sx-plan.md @@ -396,3 +396,51 @@ If performance ever becomes a concern, WASM is the escape hatch at three levels: 3. **Compute-heavy primitives in WASM.** Keep the sx interpreter in JS, bind specific primitives to WASM (image processing, crypto, data transformation). Most pragmatic and least disruptive — additive, no architecture change. The primitive-binding model means the evaluator doesn't care what's behind a primitive. `(blur-image data radius)` could be a JS Canvas call today and a WASM JAX kernel tomorrow. The sx source doesn't change. + +### Server-Driven by Default: The React Question + +The sx system is architecturally aligned with HTMX/LiveView — server-driven UI — even though it does far more on the client (full s-expression evaluation, DOM rendering, morph reconciliation, component caching). The server is the single 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. + +React's client-state model (`useState`, `useEffect`, Context, Suspense) exists because React was built for SPAs that need to feel like native apps — optimistic updates, offline capability, instant feedback without network latency. But it created an entire category of problems: state management libraries, hydration mismatches, cache invalidation, stale closures, memory leaks from forgotten cleanup, the `useEffect` footgun. + +**The question is not "should sx have useState" — it's which specific interactions actually suffer from the server round-trip.** + +For most of our apps, that's a very short list: +- Toggle a mobile nav panel +- Gallery image switching +- Quantity steppers +- Live search-as-you-type + +These don't need a general-purpose reactive state system. They need **targeted client-side primitives** that handle those specific cases without abandoning the server-driven model. + +**The dangerous path:** Add `useState` → need `useEffect` for cleanup → need Context to avoid prop drilling → need Suspense for async state → rebuild React inside sx → lose the simplicity that makes the server-driven model work. + +**The careful path:** Keep server-driven as the default. Add explicit, targeted escape hatches for interactions that genuinely need client-side state. Make those escape hatches obviously different from the normal flow so they don't creep into everything. + +#### What sx has vs React + +| React feature | SX status | Verdict | +|---|---|---| +| Components + props | `defcomp` + `&key` | Done — cleaner than JSX | +| Fragments, conditionals, lists | `<>`, `if`/`when`/`cond`, `map` | Done — more expressive | +| Macros | `defmacro` | Done — React has nothing like this | +| OOB updates / portals | `sx-swap-oob` | Done — more powerful (server-driven) | +| DOM reconciliation | `_morphDOM` (id-keyed) | Done — works during SxEngine swaps | +| Reactive client state | None | **By design.** Server is source of truth. | +| Component lifecycle | None | Add targeted primitives if body.js behaviors move to sx | +| Context / providers | `_componentEnv` global | Sufficient for auth/theme; revisit if trees get deep | +| Suspense / loading | `sx-request` CSS class | Sufficient for server-driven; revisit for Phase 4 client data | +| Two-way data binding | None | Not needed — HTMX model (form POST → new HTML) works | +| Error boundaries | Global `sx:responseError` | Sufficient; per-component boundaries are a future nice-to-have | +| Keyed list reconciliation | id-based morph | Works; add `:key` prop support if list update bugs arise | + +#### Targeted escape hatches (not a general state system) + +For the few interactions that need client-side responsiveness, add **specific primitives** rather than a general framework: + +- `(toggle! el "class")` — CSS class toggle, no server trip +- `(set-attr! el "attr" value)` — attribute manipulation +- `(on-event el "click" handler)` — declarative event binding within sx +- `(timer interval-ms handler)` — with automatic cleanup on DOM removal + +These are imperative DOM operations exposed as primitives — not reactive state. They let components handle simple client-side interactions without importing React's entire mental model. The server-driven flow remains the default for anything involving data.