Skip to main content
@juo/blocks is reactive end-to-end. Block props, service state, theme tokens, and workflow status are all reactive signals — when one changes, the renderers that depend on it re-run automatically.

One reactivity model, many frameworks

@juo/blocks exports a set of reactive primitives — signal, computed, effect, untracked — that work the same way regardless of which renderer you use. The API follows the TC39 Signals proposal, so the mental model will feel familiar. Each renderer bridges these primitives in the way that feels most natural for that framework:
RendererPattern
Vanillasignal(), effect(), computed() directly
VueSignals appear as Refs through the useContext proxy — use them like any Vue ref
ReactuseSignal(signal) returns the current value and re-renders on change
PreactuseSignal(signal) — same shape as React, Preact’s hooks runtime
A service can update a signal from anywhere — a fetch callback, a websocket, a workflow response — and the change propagates through every renderer’s output without manual wiring.

Core primitives

These are re-exported from @juo/blocks for use in vanilla renderers, services, and tests.

signal(initialValue)

A writable observable. Reads inside an effect or computed register a dependency automatically.
import { signal } from "@juo/blocks";

const count = signal(0);
count.value++;            // triggers dependents
console.log(count.value); // 1

computed(fn)

A read-only derived value, recomputed lazily.
import { signal, computed } from "@juo/blocks";

const price = signal(1000);
const tax = computed(() => price.value * 0.21);

tax.value; // 210

effect(fn) + untracked(fn)

effect runs the callback immediately and re-runs it whenever a signal it read from changes. untracked reads a signal value without registering it as a dependency — useful when the read is incidental to the work.
import { effect, untracked } from "@juo/blocks";

effect(() => {
  const status = subscription.status.value;  // tracked
  untracked(() => log("status changed", status));
});

Where signals show up

Signals appear in three places:
  1. Service state. Every service exposes its mutable state as signals — subscriptionService.current, workflowService.canRespond, customerService.current. See Services.
  2. Theme tokens. The active theme palette is a signal, so blocks re-render when the merchant changes the theme.
  3. Block props. Inside renderers, block.props is reactive — wrap reads in effect() to keep the DOM in sync.

Vanilla example

A renderer that prints the current customer’s name and updates whenever the service signal changes:
import { defineBlock, effect, untracked, injectContext } from "@juo/blocks";
import { CustomerServiceContext } from "@juo/blocks";

defineBlock("Greeting", {
  group: "theme",
  schema: { /* ... */ } as const,
  initialValue: () => ({ props: {} }),
  renderer(block) {
    const el = document.createElement("p");
    const customer = injectContext(el, CustomerServiceContext);

    effect(() => {
      const name = customer.current.value?.name ?? "guest";
      untracked(() => {
        el.textContent = `Welcome back, ${name}`;
      });
    });

    return el;
  },
});
When the host fetches the customer (customer.getCurrent()), the signal updates and the <p> re-renders. No manual subscription, no glue code.

Framework bridges

Framework adapters handle the bridging. Calling signal.value manually inside components is not required:
  • Vue. useContext() returns a proxy that converts each Signal<T> field into a Ref<T>.
  • React / Preact. useSignal(signal) reads the value and subscribes the component to changes.
See the Vue, React, and Preact guides for the framework-specific shape.

Creating reactive state in a service

When a service needs to expose reactive values, create signals internally and return them:
import { signal, computed } from "@juo/blocks";

export function createCartService() {
  const items = signal<CartItem[]>([]);
  const total = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.quantity, 0),
  );

  return {
    items,
    total,
    add(item: CartItem) { items.value = [...items.value, item]; },
  };
}
Consumers in any framework see the same reactive values and remain synchronized without additional wiring.