Props
Props are the input a component gets from whoever uses it. The parent passes them
in like attributes, and they arrive on this.props.
export class Greeting extends Component<{ name: string }> {
render() {
return <p>Hello {this.props.name}</p>;
}
}
<Greeting name="Ada" />
The <{ name: string }> after Component describes the props this component expects,
so using it without name, or with the wrong type, is caught as you type.
Props are read-only
A component may read its props but not change them — they belong to the parent. Assigning to one throws:
this.props.name = "Grace"; // ✗ throws (RMD004)
It throws in every build, on purpose. The alternative — quietly ignoring the write — lets bugs hide: you would assign, read back the old value, and get no hint why.
If you need to change something a prop gave you, there are two honest ways:
- Make it your own state. Copy it in
@created:@state name = this.props.name. Now it is yours to change. - Ask the parent. Take a callback prop and call it — the parent owns the data, so the parent changes it.
export class Row extends Component<{ item: Item; onRemove: (id: string) => void }> {
remove() {
this.props.onRemove(this.props.item.id);
}
render() {
return <button onclick={this.remove}>remove</button>;
}
}
A prop change re-renders the component
When the parent re-renders and passes a new set of props, Ramonda compares it to the
old set. If any prop differs, it re-renders the whole component — the same coarse
rule as state, and for the same reason: a changed prop almost
always changes what the component shows. It does not matter which props render()
actually reads; a change to any of them re-renders.
The comparison is shallow — each prop by === — so passing the same values again
costs nothing, and re-renders only when something really changed. Three ways to go
finer when you need to:
-
To pass an object or an array written in the JSX, declare it a value with
@StableProps— see below. -
To ignore a change that really happened, gate it with
@ShouldUpdateOnPropsChange. That is a narrower thing than it sounds, and narrower than it used to be: a prop merely REBUILT with the same contents is@StableProps' job above, and this one is for the case where the contents genuinely moved and you still do not want the update. Refusing it drops it whole: the props are not taken either, so a later render caused by this component's own state still shows the props it last accepted, until the parent sends an update the rule agrees to.It takes a predicate rather than names, which is why it is the last thing to reach for: a rule can be wrong in the direction that matters — a component that stops rendering when it should — and nothing will report it.
-
To react to one specific prop — recompute a total, refetch when an
idchanges — read it inside a@compute, a@watchProp(below), or an a subscription; those do track the individual props they read, exactly like state.
A prop that is a value
An object written in the JSX is a new object every render, so a shallow comparison sees a change every time and the child re-renders forever:
<Panel filter={{ q: "open" }} />
The component that RECEIVES it can say that the prop is a value rather than an identity, and the framework then hands it back the object it already had for as long as the contents match:
@StableProps("filter")
export class Panel extends Component<{ filter: { q: string } }> {}
Now the call site writes the plain literal and the child is not re-rendered at all. Contents that really move still reach it — a declaration is not a freeze.
It takes as many names as you like — @StableProps("filter", "flags") — and a subclass that
declares more adds to what its parent declared.
Why the receiving component declares it, and not the call site. Whether a prop is a value or an
identity is that component's knowledge, and declaring it once settles every call site. It is also
why this takes NAMES rather than a rule: @ShouldUpdateOnPropsChange takes a predicate, and a
predicate is a thing an app can get wrong in the direction that matters — a component that stops
rendering when it should. The worst a wrong name here can do is fail to type-check.
A function is not settled by it. Two closures with the same body are not equal by any
comparison that is safe to make, so a listed function prop is left exactly as it came. Pass a bound
method — onSelect={this.select} — or @memoized when it has to be built per row.
Children are a prop, and they can be declared too. A rendered node is built during the render,
so anything a parent writes between the tags is a fresh value every time — a component given
children re-renders whenever its parent does, even when the children are a piece of static text.
The same goes for a node handed over as a prop, header={<Header />}. Both are named like any
other prop:
@StableProps("children", "header")
export class Panel extends Component<{ header?: unknown; children?: unknown }> {}
A slot that takes the component CLASS rather than a rendered node — view={Header} — costs nothing
to begin with, because a class is the same reference for the life of the module.
Contents are compared to a bounded depth, so a deeply nested literal gets a fresh reference rather than a wrong one, which is the safe direction: it re-renders, exactly as it does today.
Reacting to a specific prop changing
Sometimes you need to do something when one prop changes — refetch when an id
changes, say. @watchProp runs a method just before the render, whenever a prop
you name changes:
@watchProp((props) => props.userId)
reload([next]: [string], [previous]: [string]) {
this.data = undefined;
void this.fetch(next);
}
The values arrive as a tuple, one entry per selector, which is why the parameters above are destructured. Name several selectors and the method runs once when any of them changed:
@watchProp((props) => props.page, (props) => props.term)
reload(next: [number, string], previous: [number, string]) {
void this.fetch(next[0], next[1]);
}
Not once per changed prop — once per update in which at least one moved. A selector whose value did not
change keeps it in both arrays, so previous[i] === next[i] tells you which one moved.
showing adano change yet — @watchProp does not fire on mount
- It doesn't run on the first render — only on a later change. Use
@createdfor the initial load. - The selector needs no annotation.
propsis typed from the class the decorator is on, soprops.usreIdis a compile error. (The method'snext/previousdo still need annotating — a decorator cannot type the signature it decorates, which is a TypeScript limitation rather than a choice.) - It's a selector function, not a string — so it can go as deep as
p => p.filters[0].value, and the compiler checks it. - On a hook it watches the HOOK's props — the bag its
this.use()callback produced, not the owner's.
@watchProp or @updated? (optional)
Both react to a change; they differ in when. @watchProp runs before the render,
so what it derives is on screen in the same pass. @updated runs
after the page updates — the place for reading or correcting the DOM, not for working
out what to show.
There is no post-commit @watchProp, and that is a decision rather than a gap:
the lifecycle page explains it. Short version — a post-commit
reaction cannot fold its state write into the render, so the framework would have to start
comparing your props for you, and @updated plus one field comparison already does the
job without that.
children is a prop
The content between a component's tags arrives as the children prop:
export class Panel extends Component<{ children?: RamondaNode }> {
render() {
return <section className="panel">{this.props.children}</section>;
}
}
Next
- Lifecycle —
@created,@mounted,@destroyed, and their order. - The decorator table —
@watchPropworks on a hook;@ShouldUpdateOnPropsChangedoes not.