Ramonda

Writing a hook

A hook takes an options type and reads it from this.options.

interface CounterOptions {
  start: number;
}

export class Counter extends Hook<CounterOptions> {
  @state count = 0;

  @create
  seed() {
    this.count = this.options.start;
  }

  increment() {
    this.count = this.count + 1;
  }
}

Passing options

Two forms, and the difference matters.

// A plain bag — fixed for the life of the hook.
counter = this.use(Counter, { start: 10 });

// A callback — re-evaluated when the owner re-renders.
counter = this.use(Counter, (self: Panel) => ({ start: self.props.initial }));

Reach for the callback whenever the options depend on anything that changes. The bag form is for constants.

Options are read-only, and they throw

this.options.start = 5;   // TypeError, RMD015

They belong to the caller. Assigning throws in every build — the same rule and the same reasoning as props, because they are the same thing seen from the other side: one rule for read-only inputs, not two.

Copy into your own @state, or take a callback option and ask the owner to change it.

How they stay reactive

Each option key is backed by its own signal on the owner's runtime. When the owner re-renders, the callback is re-evaluated and only the keys whose values actually moved are updated.

So a hook that reads this.options.start reacts to start changing and not to some other key changing on the same bag. Reads are per key, exactly like props.

An option a caller stops passing is set to undefined rather than keeping its last value — a removed key would otherwise be invisible forever.

Hooks compose

A hook may use hooks:

export class Pagination extends Hook<PaginationOptions> {
  private route = this.use(RouteHook);
  ...
}

They share the owner's runtime all the way down, so the whole chain re-renders the same component.

Testing one

import { renderHook, act } from "@ramonda/testing-library";

const { current, rerender } = renderHook(Counter, { initialOptions: { start: 2 } });
expect(current.count).toBe(2);

act(() => current.increment());
expect(current.count).toBe(3);

rerender({ start: 99 });   // drives the same option signals a parent would

current does not change between renders — a hook is constructed once by use() and lives as long as its owner. The instance is the identity; the fields are what change.

Next