Ramonda

Classes and decorators

Why a class

A component keeps three things together: the data it remembers, the code that changes that data, and the code that draws it. A class is a natural home for all three — they share one this, so they can reach each other without being passed around, and the component has a real identity that survives re-renders. State is just a field; there is no separate place for it to live and no rules about when you may read it.

It also makes reuse cheap. A component is not an element — it puts on the page exactly what its render() returns — so a component wrapped around another contributes no node of its own, and composing is free rather than something to ration. And because behaviour lives on a class, the same class shape carries it without a render at all: that is a hook, and it is how state, a lifecycle or a subscription is reused between components that share no markup.

Why no fragment

There is nothing to group with, because a component is not an element. What its render() returns goes on the page as it stands — one element, several, or none, whatever the render says — so there is no wrapper to avoid and no invisible tag to reach for.

class Cells extends Component<{ name: string; score: number }> {
  render() {
    return [<td>{this.props.name}</td>, <td>{this.props.score}</td>];
  }
}
<tr><td>Ada</td><td>9</td></tr>

Two cells, from one component, with nothing between them. It is still a real component while it does this: it can hold state, a lifecycle and hooks, and there may be many instances of it.

So a component covers every case a fragment does, and one it does not: a fragment holds no state, so a component that exists only to decide what to show is something a fragment cannot be.

class WhenOpen extends Component {
  @state open = false;

  toggle() {
    this.open = !this.open;
  }

  render() {
    return [
      <button onclick={this.toggle}>{this.open ? "hide" : "show"}</button>,
      this.open ? <p>Now you see it.</p> : null,
    ];
  }
}

That is a button and, sometimes, a paragraph. Closed, it is a live component with state and one node; open, two — and either way there is nothing of the framework's around it.

Why not a function component

A plain function in tag position is refused, and for a different reason than a fragment was. A function has nothing to construct, no state, and no lifecycle — so as a tag it names nothing the framework can keep hold of, and <Thing /> and Thing() would mean the same thing written two ways.

For markup you want to reuse without state, call the function in an expression slot — {sideBar()} — where it reads as the value it is.

TypeScript catches most of this on its own: JSX.ElementType is deliberately left undeclared, so the compiler's default rule applies — a tag must return one JSX.Element — and a function returning several nodes, or anything that is not a node, is refused (TS2786). The one it lets through is a function returning exactly ONE element, which is also the way somebody writes a function component out of habit. That one is RMD011.

Why decorators, not reserved method names

Lifecycle and state are decorators — @state, @created, @mounted, @updated — rather than special method names you are expected to implement.

The reason is collisions. If "the method named mount runs on mount", then mount is a name you can never use for anything else, on any component, ever — and nothing warns you when you shadow it by accident. A decorator attaches the behaviour without claiming the name: @mounted ready() runs on mount, and ready is still just a method you named. (render is the one exception — it is abstract, so the compiler forces exactly one and the collision can't happen.)

Why your methods are already bound

onclick={this.increment} works — you don't write a constructor or an arrow-field to keep this. Ramonda binds your methods to the instance when the component is built.

That is not only convenience: a decorator can't be applied to an arrow-function field, so if the way to keep this were an arrow field, you couldn't decorate your handlers. Binding ordinary methods is what keeps @memoized and the event decorators usable on them.

Why TC39 decorators

Ramonda uses the standard (TC39 stage-3) decorators — the ones on their way into JavaScript itself — not the older experimental TypeScript ones. That is why installation asks your bundler to transpile them: they are new enough that not every runtime parses them yet, but they are the version with a future.

Next