packages/component/docs/composition.md
Building component trees with props, children, refs, and keys.
Props flow from parent to child through JSX attributes:
function Parent() {
return () => <Child message="Hello from parent" count={42} />
}
function Child() {
return (props: { message: string; count: number }) => (
<div>
<p>{props.message}</p>
<p>Count: {props.count}</p>
</div>
)
}
Components can compose other components via children:
function Layout() {
return (props: { children: RemixNode }) => (
<div mix={[css({ padding: '20px', maxWidth: '1200px', margin: '0 auto' })]}>
<header>My App</header>
<main>{props.children}</main>
<footer>© 2024</footer>
</div>
)
}
function App() {
return () => (
<Layout>
<h1>Welcome</h1>
<p>Content goes here</p>
</Layout>
)
}
Use the ref(...) mixin to get a reference to the DOM node after it's rendered. This is useful for DOM operations like focusing elements, scrolling, measuring dimensions, or setting up observers.
function Form(handle: Handle) {
let inputRef: HTMLInputElement
return () => (
<form>
<input type="text" mix={[ref((node) => (inputRef = node))]} />
<button
mix={[
on('click', () => {
// Focus the input from elsewhere in the form
inputRef.focus()
}),
]}
>
Focus Input
</button>
</form>
)
}
The ref callback receives an AbortSignal as its second parameter, which is aborted when the element is removed from the DOM. Use this for cleanup operations:
function ResizeTracker(handle: Handle) {
let dimensions = { width: 0, height: 0 }
return () => (
<div
mix={[
ref((node, signal) => {
// Set up ResizeObserver
let observer = new ResizeObserver((entries) => {
let entry = entries[0]
if (entry) {
dimensions.width = Math.round(entry.contentRect.width)
dimensions.height = Math.round(entry.contentRect.height)
handle.update()
}
})
observer.observe(node)
// Clean up when element is removed
signal.addEventListener('abort', () => {
observer.disconnect()
})
}),
]}
>
Size: {dimensions.width} x {dimensions.height}
</div>
)
}
The ref callback is called only once when the element is first rendered, not on every update.
Use the key prop to uniquely identify elements in lists. Keys enable efficient diffing and preserve DOM nodes and component state when lists are reordered, filtered, or updated.
function TodoList(handle: Handle) {
let todos = [
{ id: '1', text: 'Buy milk' },
{ id: '2', text: 'Walk dog' },
{ id: '3', text: 'Write code' },
]
return () => (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
When you reorder, add, or remove items, keys ensure:
function ReorderableList(handle: Handle) {
let items = [
{ id: 'a', label: 'Item A' },
{ id: 'b', label: 'Item B' },
{ id: 'c', label: 'Item C' },
]
function reverse() {
items = [...items].reverse()
handle.update()
}
return () => (
<div>
<button mix={[on('click', reverse)]}>Reverse List</button>
{items.map((item) => (
<div key={item.id}>
<input type="text" defaultValue={item.label} />
</div>
))}
</div>
)
}
Even when the list order changes, each input maintains its value and focus state because the key prop identifies which DOM node corresponds to which item.
Keys can be any type (string, number, bigint, object, symbol), but should be stable and unique within the list:
// Good: stable, unique IDs
{
items.map((item) => <Item key={item.id} item={item} />)
}
// Good: index can work if list never reorders
{
items.map((item, index) => <Item key={index} item={item} />)
}
// Bad: don't use random values or values that change
{
items.map((item) => <Item key={Math.random()} item={item} />)
}