Overview

Scopes share reactive state from a parent component to any descendant — at any depth — without prop drilling. The parent calls scope(ScopeClass) to instantiate and register a class; descendants call getParentScope(ScopeClass) to retrieve the nearest ancestor instance. Internally, retrieval bubbles a CustomEvent up the DOM tree.

Define atoms and mutation methods on the scope class. Methods are automatically bound so they can be passed as event handlers without losing this. An optional setup(propsFn) on the class receives the host component’s props function and can register lifecycle hooks (onMount, onCleanup, etc.).

App-wide utilities (router atoms, fetch client, storage bags, theme/title) live on the built-in MatesUtils store. Read them with utils (or useUtils()) — do not import those atoms as globals.

vs React Context

Aspect
Mates scope
Notes
Provide scope(Class) Instantiates the class on the host; available to all descendants.
Consume getParentScope(Class) Nearest ancestor match; throws if missing — no default value API.
Shape Class instance Atoms + methods on the instance; direct method calls instead of dispatch/reducers.

Sharing state across child components

Both Display and Controls call getParentScope(CounterScope) — they both receive the same instance created by App's scope(CounterScope). Changes in Controls instantly reflect in Display, with no props passed between them.

import { html, atom, x, renderApp, scope, getParentScope } from 'mates';

// Define a scope class — atoms and methods live here
class CounterScope {
count = atom(0);
label = atom('Clicks');
increment() { this.count.set(n => n + 1); }
reset() { this.count.set(0); }
}

// Child component — reads from parent's scope
const Display = () => {
const counter = getParentScope(CounterScope);
return () => html`
<div class="card card-body p-10 center">
<h1>${counter.count()}</h1>
<span class="label m-t-5">${counter.label()}</span>
</div>
`;
};

// Another child — also reads from parent's scope
const Controls = () => {
const counter = getParentScope(CounterScope);
return () => html`
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${counter.increment}>+1</button>
<button class="btn-ghost" @click=${counter.reset}>Reset</button>
</div>
`;
};

// Parent — creates the scope
const App = () => {
const counter = scope(CounterScope); // creates scope in outer fn

Nested scopes shadow the parent

A child that calls scope(ThemeScope) creates a new instance. Descendants under that child see the nested instance; siblings under App still see the parent.

import { html, atom, x, renderApp, scope, getParentScope } from 'mates';

class ThemeScope {
accent = atom('teal');
setAccent(c) { this.accent.set(c); }
}

const Swatch = () => {
const theme = getParentScope(ThemeScope);
return () => html`
<div class="card card-body p-10 center"
style="border-color:${theme.accent()}">
<span class="label">accent=${theme.accent()}</span>
</div>
`;
};

// Child provider — shadows parent's ThemeScope for its descendants
const NestedPanel = () => {
const theme = scope(ThemeScope); // fresh instance for this subtree
theme.accent.set('coral');
return () => html`
<div class="m-col m-gap">
<p class="muted m-t-5">Nested scope (coral)</p>
${x(Swatch)}
</div>
`;
};

const App = () => {
const theme = scope(ThemeScope);
return () => html`
<div class="m-col m-gap">
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${() => theme.setAccent('teal')}>teal</button>
<button @click=${() => theme.setAccent('violet')}>violet</button>