DEV Community

Cover image for Server-First Web Component Architecture: SXO + Reactive Component
Víctor García
Víctor García

Posted on

Server-First Web Component Architecture: SXO + Reactive Component

Web components offer native UI primitives. However, they often introduce complex lifecycles, Shadow DOM issues, and verbose code.

SXO + Reactive Component solves this. It combines server-side rendering with vanilla JSX and a signal-based reactive system. You get declarative authoring, instant page loads, and progressive enhancement without hydration.

Server-First, Reactivity-Second

SXO inverts the traditional model:

  1. Server: Renders semantic HTML wrapped in custom elements (e.g., <product-card>).
  2. Client: A lightweight reactive runtime (~4.8KB) binds state and behavior to the existing DOM.

There is no hydration pass. The HTML is the source of truth.

Core Concepts

Reactive Components use $-prefixed attributes to link HTML to behavior.

  • $state="key": Initializes state from text content. Updates reflect automatically.
  • $bind-*="key": Two-way binds attributes (like value or checked) to state.
  • $on*="method": Binds events to client-side handlers.
  • define(tag, setup): Defines client logic, state, effects ($effect), and events ($on).

Product Card

Lets create a product card with a quantity selector and a "Favorite" toggle.

1. Server Component (JSX)

Renders accessible HTML on the server. No client-side logic is included here.

// src/components/product-card.jsx
export function ProductCard({ title, price, image }) {
  return (
    <product-card class="card">
      <img src={image} alt={title} />
      <h3>{title}</h3>
      <p>${price}</p>

      <div class="controls">
        <button type="button" $onclick="decrement" aria-label="Decrease">-</button>
        <input type="number" $bind-value="qty" value="1" min="1" aria-label="Qty" />
        <button type="button" $onclick="increment" aria-label="Increase">+</button>
      </div>

      <button
        type="button"
        $onclick="toggleFavorite"
        $bind-attr="favoriteAttrs"
        aria-pressed="false"
        class="btn-icon"
      ></button>
    </product-card>
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Client Enhancer

Attaches behavior to the server-rendered markup.

// src/components/product-card.client.js
import { define } from "@qery/reactive-component";

define("product-card", ({ $state, $on, $compute }) => {
  // 1. Initialize State
  $state.qty = 1;
  $state.isFavorite = false;

  // 2. Compute aria-pressed attribute declaratively
  //    and falsy values remove them
  $compute("favoriteAttrs", ["isFavorite"], (isFavorite) => ({
    "aria-pressed": isFavorite ? "true" : "false",
  }));

  // 3. Define Actions
  $on.increment = () => $state.qty++;
  $on.decrement = () => {
    if ($state.qty > 1) $state.qty--;
  };

  $on.toggleFavorite = () => {
    $state.isFavorite = !$state.isFavorite;
  };
});
Enter fullscreen mode Exit fullscreen mode

3. Page Composition

Compose the component in a server route.

// src/pages/products/index.jsx
import { ProductCard } from "../../components/product-card.jsx";

export default function ShopPage({ products }) {
  return `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <title>Shop</title>
        <link rel="stylesheet" href="/styles.css">
      </head>
      <body>
        <main>
          ${products.map(p => ProductCard(p)).join('')}
        </main>
      </body>
    </html>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Key Advantages

  • Instant Interaction: The UI is visible immediately. No loading spinners or layout shifts.
  • Simple State: Signal-based state management without complex providers.
  • Zero-Config: Automatic bundling of per-route client entries.

Resources

Top comments (1)

Collapse
 
aarongustafson profile image
Aaron Gustafson

I like the concept, but would love to see it taken further. If JS fails, it doesn’t look like there is a server-side fallback. Were this a true progressive enhancement, the increment/decrement and favoriting logic would have a fallback server path — e.g., form submission — that is overloaded with the JS-bound behavior. And any actions that are purely JS-driven — such as geolocation — should be injected via JavaScript, once you know they can run.

If anything causes JavaScript execution on the client to stop (which can and does happen), users will be shown UI that is non-functional, which is frustrating at best and absolutely leads to loss of sales, leads, etc.

If there is a critical task users must be able to do, you should have a fallback strategy for enabling them to do it no matter what.