Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/remix/llms.txt
Use this file to discover all available pages before exploring further.
Component Rendering
Remix components follow a two-phase rendering model: setup once, then render on every update.
Component Lifecycle
First Render
- Component function is called with
handle and setup prop
- Setup phase runs once to initialize state
- Returned render function is stored
- Render function is called with props
- Queued tasks execute after rendering
Subsequent Updates
- Only the render function is called (setup is skipped)
- Props are passed to the render function
- The
setup prop is excluded from props
- Queued tasks execute after rendering
Component Removal
handle.signal is aborted
- Event listeners registered via
handle.on() are cleaned up
- Queued tasks execute with an aborted signal
Basic Rendering
The simplest component returns JSX:
function Greeting() {
return (props: { name: string }) => (
<div>Hello, {props.name}!</div>
)
}
let element = <Greeting name="World" />
Rendering with State
Use handle.update() to trigger re-renders:
import type { Handle } from 'remix/component'
import { on } from 'remix/component'
function Counter(handle: Handle) {
let count = 0
return () => (
<div>
<span>Count: {count}</span>
<button mix={[on('click', () => {
count++
handle.update()
})]}>
Increment
</button>
</div>
)
}
Prop Passing
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>
)
}
Conditional Rendering
Use JavaScript expressions for conditional rendering:
function Toggle(handle: Handle) {
let isOpen = false
return () => (
<div>
<button mix={[on('click', () => {
isOpen = !isOpen
handle.update()
})]}>
Toggle
</button>
{isOpen && (
<div>Content is visible</div>
)}
</div>
)
}
Lists and Keys
Use the key prop to identify list items:
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>
)
}
Why Keys Matter
Keys enable efficient diffing and preserve:
- DOM nodes - Elements with matching keys are reused, not recreated
- Component state - Component instances persist across reorders
- Focus and selection - Input focus stays with the same element
- Form values - Input 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 mix={[on('click', reverse)]}>
Reverse List
</button>
{items.map((item) => (
<div key={item.id}>
<input type="text" defaultValue={item.label} />
</div>
))}
</div>
)
}
Key Guidelines
// 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} />)}
Composition Through Children
Components can compose other components via children:
import type { RemixNode } from 'remix/component'
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>
)
}
Fragment for Grouping
Use Fragment to group elements without adding DOM nodes:
import { Fragment } from 'remix/component'
function List() {
return () => (
<Fragment>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</Fragment>
)
}
// Or use the shorthand syntax
function List() {
return () => (
<>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</>
)
}
Async Updates
Wait for updates to complete before performing DOM operations:
function Player(handle: Handle) {
let isPlaying = false
let playButton: HTMLButtonElement
let stopButton: HTMLButtonElement
return () => (
<div>
<button
disabled={isPlaying}
mix={[
ref((node) => (playButton = node)),
on('click', async () => {
isPlaying = true
await handle.update()
// Focus the enabled button after update completes
stopButton.focus()
}),
]}
>
Play
</button>
<button
disabled={!isPlaying}
mix={[
ref((node) => (stopButton = node)),
on('click', async () => {
isPlaying = false
await handle.update()
// Focus the enabled button after update completes
playButton.focus()
}),
]}
>
Stop
</button>
</div>
)
}
Queued Tasks
Use handle.queueTask() for work that needs to happen after rendering:
import { ref, on } from 'remix/component'
function Form(handle: Handle) {
let showDetails = false
let detailsSection: HTMLElement
return () => (
<form>
<label>
<input
type="checkbox"
checked={showDetails}
mix={[on('change', (event) => {
showDetails = event.currentTarget.checked
handle.update()
if (showDetails) {
// Scroll after the section renders
handle.queueTask(() => {
detailsSection.scrollIntoView({ behavior: 'smooth' })
})
}
})]}
/>
Show details
</label>
{showDetails && (
<section
css={{ marginTop: '2rem', padding: '1rem' }}
mix={[ref((node) => (detailsSection = node))]}
>
<h2>Additional Details</h2>
</section>
)}
</form>
)
}
Context for Indirect Composition
Use context to share values without prop drilling:
import type { Handle, RemixNode } from 'remix/component'
function ThemeProvider(handle: Handle<{ theme: 'light' | 'dark' }>) {
let theme: 'light' | 'dark' = 'light'
handle.context.set({ theme })
return (props: { children: RemixNode }) => (
<div>
<button mix={[on('click', () => {
theme = theme === 'light' ? 'dark' : 'light'
handle.context.set({ theme })
handle.update()
})]}>
Toggle Theme
</button>
{props.children}
</div>
)
}
function ThemedContent(handle: Handle) {
let { theme } = handle.context.get(ThemeProvider)
return () => (
<div css={{ backgroundColor: theme === 'dark' ? '#000' : '#fff' }}>
Current theme: {theme}
</div>
)
}
TypedEventTarget for Efficient Updates
For better performance, use TypedEventTarget to avoid updating the entire subtree:
import { TypedEventTarget } from 'remix/component'
class Theme extends TypedEventTarget<{ change: Event }> {
#value: 'light' | 'dark' = 'light'
get value() {
return this.#value
}
setValue(value: 'light' | 'dark') {
this.#value = value
this.dispatchEvent(new Event('change'))
}
}
function ThemeProvider(handle: Handle<Theme>) {
let theme = new Theme()
handle.context.set(theme)
return (props: { children: RemixNode }) => (
<div>
<button mix={[on('click', () => {
// No update needed - consumers subscribe to changes
theme.setValue(theme.value === 'light' ? 'dark' : 'light')
})]}>
Toggle Theme
</button>
{props.children}
</div>
)
}
function ThemedContent(handle: Handle) {
let theme = handle.context.get(ThemeProvider)
// Subscribe to granular updates
handle.on(theme, {
change() {
handle.update()
},
})
return () => (
<div css={{ backgroundColor: theme.value === 'dark' ? '#000' : '#fff' }}>
Current theme: {theme.value}
</div>
)
}
Next Steps