Skip to main content
Contexts let any block in the tree access a shared value — a service, a state signal, the active workflow — without prop drilling and without a global store. Values flow through the DOM via a W3C-style request/response protocol implemented by the <juo-context-root> custom element.
<juo-context-root>             ← host provides contexts here
  └─ <juo-page>                ← renders the current route's root block
       └─ <juo-extension-root> ← slot fan-out
            └─ <juo-block>     ← blocks request contexts via injectContext

Providing a context

Call provideContext() once at startup, before blocks render:
import {
  provideContext,
  SubscriptionServiceContext,
  createSubscriptionService,
  createApiSubscriptionAdapter,
} from "@juo/blocks";

const root = document.querySelector("juo-context-root") as HTMLElement;

provideContext(
  root,
  SubscriptionServiceContext,
  createSubscriptionService(createApiSubscriptionAdapter({ baseUrl: "/api/v1" })),
);
See Services for the full list of contexts the platform expects.

Consuming a context

In a vanilla renderer, use injectContext() for a one-shot synchronous read against the surrounding DOM:
import { injectContext, SubscriptionServiceContext } from "@juo/blocks";

renderer(block) {
  const el = document.createElement("div");
  const subscription = injectContext(el, SubscriptionServiceContext);
  // ...
  return el;
}
Framework components use a useContext composable / hook that wires signals into native reactivity — see the Vue, React, and Preact guides.

Reactive updates

If the provided value can change over time, use subscribeContext:
import { subscribeContext, ThemeStateContext } from "@juo/blocks";

const unsubscribe = subscribeContext(element, ThemeStateContext, (theme) => {
  // Called immediately with the current value, then on each change
  applyTheme(el, theme.globalStyles.value);
});

// Call unsubscribe() when the element is removed
In Vue/React/Preact components, this isn’t needed — the framework useContext already bridges signal updates into the host framework’s reactivity. See Reactivity.

Scoping and overrides

Every <juo-context-root> element captures context-provide and context-request events at its own boundary — providers are stored on the root that catches the event, and requests are answered by the nearest root above the requester. That makes contexts naturally scoped. Any context can be overridden for a sub-tree by nesting a second <juo-context-root> and providing a different value on it. Contexts that the inner root does not re-provide fall through to the outer scope.
<juo-context-root id="app-root">
  <juo-context-root id="preview-root">
    <juo-block name="SubscriptionList"></juo-block>
  </juo-context-root>
</juo-context-root>
const appRoot = document.getElementById("app-root")!;
const previewRoot = document.getElementById("preview-root")!;

// ContextA and ContextB live on the outer root
provideContext(appRoot, ContextA, valueA);
provideContext(appRoot, ContextB, valueB);

// The inner root overrides ContextA only
provideContext(previewRoot, ContextA, valueAOverride);

// Inside the inner root:
//  injectContext(el, ContextA) → valueAOverride (stops at the nearest root)
//  injectContext(el, ContextB) → valueB         (bubbles up to the outer root)
Outer green juo-context-root contains two rose provideContext rectangles labelled ContextA and ContextB. Inside, a nested green juo-context-root labelled preview-root contains its own rose provideContext ContextA (overriding the outer one) and a purple juo-block SubscriptionList holding two rose injectContext rectangles, one for ContextA and one for ContextB. Two dashed rose arrows labelled 'resolves A' and 'resolves B' show ContextA resolving to the inner override while ContextB bubbles up past the inner root to the outer provideContext. Typical uses:
  • Previews and Storybook. Wrap a block in a nested root that provides mock services.
  • Workflow overlays. The flow overlay creates its own root so it can swap in a flow-specific WorkflowService and PageContext without disturbing the rest of the page.
  • Tenant or surface scoping. Different sections of the page can run against different adapters.
Overrides are per-context — anything not re-provided on the inner root still falls through to the outer scope, because the inner root has no entry for that context and the request bubbles up.

Defining a custom context

import { createContext, provideContext, injectContext } from "@juo/blocks";

// Define the key once, export it from a shared module
export const CartContext = createContext<CartService>("cart");

// Provide at root
provideContext(rootElement, CartContext, myCartService);

// Inject anywhere in the tree
const cart = injectContext(element, CartContext);
@juo/blocks ships context keys for every built-in serviceSubscriptionServiceContext, CustomerServiceContext, and so on. Define a custom context with createContext when sharing something beyond the built-in services.