Ramonda

RMD055 — A hook's props passed as a plain object

class Panel extends Component {
  @state count = 1;

  // ✗ the compiler refuses this, and `use()` throws if it arrives anyway
  counter = this.use(Counter, { start: this.count });
}

A field initializer runs once, so an object written in one holds what was true at that moment and goes on holding it for the life of the hook. start there is 1 forever: this.count moving to 7 changes the owner and reaches nothing inside Counter.

Pass a callback, and the props follow:

counter = this.use(Counter, () => ({ start: this.count }));

The callback is cached on the signals it reads, so it is re-run on a render where one of them moved and skipped on a render where none did.

Constants are written the same way, and cost the same. A callback that reads no signal is called once, at mount, and never again, and the inline functions in it keep their identity across the owner's renders — measured in core's PropsBagRuns.test.tsx. So there is no bag cheap enough for the shape to be worth choosing.

A development build calls it more often than that, and keeps none of it: a second time at mount, so RMD022 can compare the two bags and catch a value that is not a function of state, and once per render of the owner, so RMD027 can check the cache has not gone stale. The hook is handed the first bag in every build.

It throws in every build, like a write to props (RMD004, RMD015): the alternative is a shipped bundle serving one stale value for the life of the page, silently. The report beside the throw is development-only, and it names the owner, the hook, and the keys the object carried.

The mistake cannot be found from inside use(), which is handed a finished object with no way to tell { start: this.count } from { start: 1 }. The FORM is the visible half, so the form is what the framework holds you to.

Next