Children
The content you put inside a component's tags is handed to it as a prop called
children. That lets you write a component that wraps whatever it is given.
export class Panel extends Component<{ title: string; children?: RamondaNode }> {
render() {
return (
<section className="panel">
<h2>{this.props.title}</h2>
{this.props.children}
</section>
);
}
}
<Panel title="Settings">
<p>Anything at all.</p>
</Panel>
Panel draws its own frame and drops whatever it was given where
{this.props.children} sits.
More than one slot
There is no special "slot" syntax — if you need to place content in more than one spot, take more than one prop. Markup is just a value, so you can pass it like any other:
export class Dialog extends Component<{
header: RamondaNode;
children?: RamondaNode;
footer?: RamondaNode;
}> {
render() {
return (
<div className="dialog">
<header>{this.props.header}</header>
<div className="body">{this.props.children}</div>
{this.props.footer ? <footer>{this.props.footer}</footer> : null}
</div>
);
}
}
<Dialog header={<h2>Confirm</h2>} footer={<button>OK</button>}>
<p>Are you sure?</p>
</Dialog>
One trap: passing an unkeyed list as children
If a component wraps its children in its own markup, it can't control how the caller
built those children. If the caller passes a bare .map(), items get matched by
position — and as the list grows, the wrapper's own markup can be mistaken for a list
item, so state lands on the wrong node. There is no visible error; development reports
it.
The fix is the caller's, and it is list() instead of .map():
// instead of {items.map((item) => <li>{item.name}</li>)}
<Panel>{list(items, (item) => <Item item={item} />)}</Panel>
list() keeps the rows as one child rather than letting them mix with the panel's own
markup, so nothing around them can be confused for part of the list.
It identifies a row by what sets that row apart from its siblings — which works while your
objects are the ones you built, and stops working the moment the data is replaced from
outside: a refetch or a JSON.parse hands over objects nothing has seen, so every row is
rebuilt and whatever it was holding goes with it. When that happens, say which field names a
row where the data arrives:
this.rows = merge(this.rows, await api.getRows(), (row) => row.id);
Said once, at the boundary, rather than on every list that renders those rows.
RMD051 reports a row that carries nothing to tell it apart, and
lists has the whole story.
Next
- Error boundaries — containing a failure.