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 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
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:
- DOM nodes are reused - Elements with matching keys are moved, not recreated
- Component state is preserved - Component instances persist across reorders
- Focus and selection are maintained - Input focus stays with the same element
- Input values are preserved - Form values remain with their elements
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
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} />)
}- Context - Indirect composition without prop drilling