Skip to main content
@juo/blocks/react is the React 18+ adapter. It exposes createReactRenderer and a set of hooks for context and signal interop.
pnpm add react react-dom
The patterns below extend the Blocks, Reactivity, and Contexts concept pages with React-specific equivalents.

Defining a React block

// Banner/index.ts
import { defineBlock, registerBlock } from "@juo/blocks";
import { createReactRenderer } from "@juo/blocks/react";
import { Banner } from "./Banner";

export const BannerBlock = defineBlock("Banner", {
  group: "theme",
  schema: /* ... */,
  initialValue: () => ({ props: { tone: "info", message: "Welcome back" } }),
  renderer: createReactRenderer(Banner),
});

registerBlock(BannerBlock);
// Banner/Banner.tsx
export function Banner({ tone, message }: { tone: string; message: string }) {
  return <div className={`banner banner--${tone}`}>{message}</div>;
}
createReactRenderer mounts the component inside a ContextProvider that resolves context from the surrounding <juo-context-root>. Hooks inside the tree can then call useContext and useSignal.

Reading services with useContext

useContext (the one from @juo/blocks/react, not React’s own) returns a proxy where every Signal<T> becomes a [value, setValue] tuple driven by useSyncExternalStore. Components re-render when accessed fields change:
import { useContext } from "@juo/blocks/react";
import { SubscriptionServiceContext } from "@juo/blocks";

export function SubscriptionStatus() {
  const subscription = useContext(SubscriptionServiceContext);

  // subscription.current is [Subscription | null, setter]
  const [current] = subscription.current;
  const status = current?.status ?? "unknown";

  return (
    <div>
      <p>Status: {status}</p>
      <button onClick={() => current && subscription.pause(current.id)}>
        Pause
      </button>
    </div>
  );
}
The $ accessor returns the raw service when direct signal access is required:
const subscription = useContext(SubscriptionServiceContext);
const rawSignal = subscription.$.current; // Signal<Subscription | null>

useSignal — read any signal as React state

For signals that aren’t part of a context (a service-internal signal, a computed, an externally created one), useSignal bridges to React state:
import { useSignal } from "@juo/blocks/react";
import { signal, computed } from "@juo/blocks";

const counter = signal(0);
const doubled = computed(() => counter.value * 2);

export function Counter() {
  const value = useSignal(counter);
  const x2 = useSignal(doubled);

  return (
    <button onClick={() => (counter.value = value + 1)}>
      {value} (×2 = {x2})
    </button>
  );
}

Providing contexts from React

Use useProvideContext inside a wrapper component:
import { useProvideContext } from "@juo/blocks/react";
import { CartContext, createCart } from "./cart";

export function CartProvider({ children }: { children: React.ReactNode }) {
  useProvideContext(CartContext, createCart());
  return <>{children}</>;
}