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 (0)