Ramonda

Your own decorators

@interval, @timeout, @onWindow and @onElement all share one shape: subscribe to something when the component appears, unsubscribe when it goes away. createSubscriptionDecorator lets you make your own decorator with that shape — handy for connecting to an external store or any subscribe/unsubscribe API.

import { createSubscriptionDecorator } from "@ramonda/core";

export const onStore = createSubscriptionDecorator(
  "onStore",
  (_owner, handler: (state: ThemeState) => void, store: ThemeStore) => store.subscribe(handler),
);

handler and the decorator's own arguments (store) are annotated because they are the contract: handler's signature is what the decorated method is checked against, and those annotations are the only place that shape is written down.

owner is different — it is optional, and annotating it is how a decorator demands something of the class it goes on:

export const onRowStore = createSubscriptionDecorator(
  "onRowStore",
  (owner: Component<{ id: string }>, handler: (state: ThemeState) => void, store: ThemeStore) =>
    store.subscribe(handler, owner.props.id),   // reads the real instance
);

Put that on a component without an id prop and it does not compile, with a message naming the owner type it wanted. Leave owner unannotated and the decorator works on any component or hook, which is what the built-in ones do.

Then use it like the built-in ones:

export class Panel extends Component {
  @state theme = "light";

  @onStore(themeStore)
  themeChanged(next: ThemeState) {
    this.theme = next.theme;
  }
}

The method's parameter needs its annotation (next: ThemeState), even though connect already declared the handler's shape. A decorator cannot contextually type the signature it decorates — an unannotated parameter is an implicit any (TS7006) — which is a TypeScript limitation rather than a choice, and the same one @watchProp lives with.

0

store listeners: 0 — unmounting the component removes its subscription, with nothing in the component to remember it

You write the connect — subscribe, and return the unsubscribe — and Ramonda handles the teardown. Nothing in Panel has to remember the subscription exists.

What it's for

Connecting to anything with a subscribe/unsubscribe pair: an external state store, a plain event emitter, a WebSocket, an IntersectionObserver.

The rule: return a function, or nothing

The connect must return the cleanup function — not an object.

// ✓  store.subscribe returns an unsubscribe function
(owner, handler, store) => store.subscribe(handler);

Many APIs hand back { unsubscribe } instead, which is not a function — wrap it:

const sub = store.subscribe(handler);
return () => sub.unsubscribe();

Returning the object would silently leak the subscription past unmount, so development throws and names the fix.

connect can follow a value

It runs inside an effect, so if it reads owner.props.x or a @state field, the subscription follows that value — disconnecting the old target before connecting the new. Read nothing, and it subscribes exactly once.

It works on hooks too

The requirement is a runtime, not an element, so a hook can own a subscription just like a component can.

Next