Your own decorators
@interval, @timeout, @onWindow and @onElement are all the same shape: subscribe to
something when the component mounts, unsubscribe when it goes away. createSubscriptionDecorator
is that shape, exposed.
import { createSubscriptionDecorator } from "@ramonda/core";
export const onStore = createSubscriptionDecorator(
"onStore",
(_owner, handler: (state: ThemeState) => void, store: ThemeStore) =>
store.subscribe(handler),
);
Then:
export class Panel extends Component {
@state theme = "light";
@onStore(themeStore)
themeChanged(next: ThemeState) {
this.theme = next.theme;
}
}
store listeners: 0 — unmounting the component removes its subscription, with nothing in the component to remember it
Write the connect — subscribe, return the unsubscribe — and the framework owns the teardown.
There is nothing in Panel that remembers the subscription exists.
Connecting to an external store
This is what it is for. Zustand, Redux, a plain event emitter, a WebSocket, IntersectionObserver
— anything with a subscribe/unsubscribe pair.
The rule: return a function, or nothing
// ✓
(owner, handler, store) => store.subscribe(handler)
// ✗ — an object is not a cleanup
(owner, handler, store) => store.subscribe(handler) // returns { unsubscribe }
{ unsubscribe } is a common return shape, and it is not a function. Without a check it
would be silently dropped and the subscription would outlive the component — measured: 1 listener
after mount, still 1 after unmount, page still working, nothing said a word.
Development throws on it and names the fix:
const sub = store.subscribe(handler);
return () => sub.unsubscribe();
The handler's type comes from connect
The decorated method's signature is taken from connect's handler parameter, so a method with
the wrong shape is a type error at the call site rather than a surprise at runtime.
connect may read signals
It runs inside an effect, so reading owner.props.x or a @state field makes the subscription
follow that value — disconnecting the old target before connecting the new one. Read nothing
and it subscribes exactly once.
Validating your decorator's arguments
The optional third parameter runs at class-definition time — the cheapest moment to catch a wrong argument, since a decorator's arguments are fixed at the source and can never depend on runtime data.
export const onChannel = createSubscriptionDecorator(
"onChannel",
(_owner, handler: (msg: string) => void, name: string) => subscribe(name, handler),
(name) => {
if (!name) throw new Error("[@onChannel] needs a channel name");
},
);
That is how @interval rejects a non-numeric delay where it is written, rather than on the first
commit.
It works on hooks too
The constraint is a runtime, not an element — so a hook can own a subscription just as a component can.
Next
- Rendering lists —
list(), and why it takes the key away.