Skip to main content

Component API Reference

Complete reference for @remix-run/component - a reactive component system for building interactive UIs.

Installation

npm i remix

Core Concepts

The Component package provides a two-phase component model:
  1. Setup phase - Runs once when component is created
  2. Render phase - Runs initially and on every update
Components use explicit state management with plain JavaScript variables and opt-in reactivity via handle.update().

Root Management

createRoot()

Creates a root for rendering components into a DOM container.
function createRoot(
  container: HTMLElement,
  options?: VirtualRootOptions
): VirtualRoot
container
HTMLElement
required
The DOM element to render into. All content will be replaced.
options
VirtualRootOptions
Optional configuration for the root.
options.frame
FrameHandle
Custom frame handle for the root frame.
options.scheduler
Scheduler
Custom scheduler for managing updates.
options.styleManager
StyleManager
Custom style manager for CSS handling.
VirtualRoot
object
Root instance with methods for rendering and lifecycle management.
render
(element: RemixNode) => void
Renders a component tree into the root. Can be called multiple times to update the app.
flush
() => void
Synchronously flushes all pending updates and tasks. Essential for testing.
dispose
() => void
Removes the component tree and cleans up all resources.

Example

import { createRoot } from 'remix/component'

let container = document.getElementById('app')!
let root = createRoot(container)

root.render(<App />)

// Later, update the app
root.render(<App key="updated" />)

// Clean up
root.dispose()

Testing Example

import { createRoot } from 'remix/component'

let container = document.createElement('div')
let root = createRoot(container)

root.render(<Counter />)
root.flush() // Ensure initial render completes

let button = container.querySelector('button')
button.click()
root.flush() // Flush to apply updates

expect(container.textContent).toBe('Count: 1')

createRangeRoot()

Creates a root that renders into a range between two DOM nodes (start and end markers).
function createRangeRoot(
  [start, end]: [Node, Node],
  options?: VirtualRootOptions
): VirtualRoot
start
Node
required
Start boundary node (typically a Comment node).
end
Node
required
End boundary node (typically a Comment node). Must share the same parent as start.
options
VirtualRootOptions
Optional configuration (same as createRoot).

Example

import { createRangeRoot } from 'remix/component'

let start = document.createComment('start')
let end = document.createComment('end')
document.body.append(start, end)

let root = createRangeRoot([start, end])
root.render(<App />)

Component Factory

Component Type

Components follow a factory pattern with two phases: setup and render.
type Component<Context = NoContext, Setup = undefined, Props = ElementProps> = (
  handle: Handle<Context>,
  setup: Setup,
) => (props: Props) => RemixNode
handle
Handle<Context>
required
Component handle providing access to lifecycle, context, and updates.
setup
Setup
Setup prop passed during component initialization (excluded from render props).
RenderFn
(props: Props) => RemixNode
Function returned by setup phase, called on initial render and updates.

Example

import type { Handle } from 'remix/component'

// Simple component (no setup)
function Greeting(handle: Handle) {
  return (props: { name: string }) => (
    <div>Hello, {props.name}!</div>
  )
}

// With setup prop
function Counter(handle: Handle, setup: number) {
  let count = setup // Initialize from setup prop
  
  return (props: { label: string }) => (
    <div>
      <p>{props.label}: {count}</p>
      <button mix={[on('click', () => {
        count++
        handle.update()
      })]}>
        Increment
      </button>
    </div>
  )
}

// Usage
<Counter setup={10} label="Count" />

Handle API

The Handle object provides the component’s interface to the framework.

Handle Type

interface Handle<C = Record<string, never>> {
  id: string
  context: Context<C>
  update(): Promise<AbortSignal>
  queueTask(task: Task): void
  frame: FrameHandle
  frames: {
    readonly top: FrameHandle
    get(name: string): FrameHandle | undefined
  }
  signal: AbortSignal
}

handle.id

Stable identifier per component instance. Useful for HTML APIs like htmlFor, aria-owns, etc.
id
string
Unique identifier that persists for the component’s lifetime.

Example

function LabeledInput(handle: Handle) {
  return () => (
    <div>
      <label htmlFor={handle.id}>Name</label>
      <input id={handle.id} type="text" />
    </div>
  )
}

handle.update()

Schedules a component update and returns a promise that resolves with an AbortSignal after the update completes.
update(): Promise<AbortSignal>
Promise<AbortSignal>
Promise<AbortSignal>
Promise that resolves after the update completes. The signal is aborted when the component re-renders or is removed.

Basic Example

function Counter(handle: Handle) {
  let count = 0
  
  return () => (
    <button mix={[on('click', () => {
      count++
      handle.update()
    })]}>
      Count: {count}
    </button>
  )
}

Async Example with Signal

function DataLoader(handle: Handle) {
  let data: string[] = []
  let loading = false
  
  async function load() {
    loading = true
    let signal = await handle.update()
    
    let response = await fetch('/api/data', { signal })
    if (signal.aborted) return
    
    data = await response.json()
    loading = false
    handle.update()
  }
  
  return () => (
    <button mix={[on('click', load)]}>
      {loading ? 'Loading...' : 'Load Data'}
    </button>
  )
}

handle.queueTask()

Schedules a task to run after the next update. The task receives an AbortSignal that’s aborted when the component re-renders or is removed.
queueTask(task: Task): void

type Task = (signal: AbortSignal) => void
task
(signal: AbortSignal) => void
required
Function to execute after the next update. Receives an AbortSignal that’s aborted on re-render or removal.

Use in Event Handlers

Queue DOM operations that need to happen after the next update:
function Modal(handle: Handle) {
  let isOpen = false
  let closeButton: HTMLButtonElement
  
  return () => (
    <div>
      <button mix={[on('click', () => {
        isOpen = true
        handle.update()
        // Queue focus after modal renders
        handle.queueTask(() => {
          closeButton.focus()
        })
      })]}>
        Open Modal
      </button>
      
      {isOpen && (
        <div role="dialog">
          <button mix={[ref(node => closeButton = node)]}>
            Close
          </button>
        </div>
      )}
    </div>
  )
}

Use in Render Function

Queue reactive work that responds to prop changes:
function DataLoader(handle: Handle) {
  let data: any = null
  let loading = false
  
  return (props: { url: string }) => {
    // Task is aborted if props.url changes or component is removed
    handle.queueTask(async (signal) => {
      loading = true
      handle.update()
      
      let response = await fetch(props.url, { signal })
      let json = await response.json()
      if (signal.aborted) return
      
      data = json
      loading = false
      handle.update()
    })
    
    if (loading) return <div>Loading...</div>
    if (!data) return <div>No data</div>
    return <div>{JSON.stringify(data)}</div>
  }
}

handle.signal

An AbortSignal that’s aborted when the component is disconnected from the tree.
signal
AbortSignal
Signal that’s aborted when the component is removed. Useful for cleanup operations.

Example with setInterval

function Clock(handle: Handle) {
  let interval = setInterval(() => {
    if (handle.signal.aborted) {
      clearInterval(interval)
      return
    }
    handle.update()
  }, 1000)
  
  return () => <span>{new Date().toString()}</span>
}

Example with addEventListener

function Clock(handle: Handle) {
  let interval = setInterval(handle.update, 1000)
  
  handle.signal.addEventListener('abort', () => {
    clearInterval(interval)
  })
  
  return () => <span>{new Date().toString()}</span>
}

handle.context

Context API for ancestor/descendant communication without prop drilling.
interface Context<C> {
  set(values: C): void
  get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>
  get(component: ElementType | symbol): unknown | undefined
}

context.set()

Stores values in the component’s context. Does not trigger updates automatically.
set(values: C): void
values
C
required
Values to store in context. Must match the Context type parameter.

context.get()

Retrieves context values from an ancestor component.
get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>
component
ComponentType
required
The component type to retrieve context from. Type is inferred from the component’s Handle type parameter.
ContextFrom<ComponentType>
object
Context values provided by the ancestor component.

Basic Context Example

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() // Must update to re-render tree
      })]}>
        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>
  )
}

Granular Updates with TypedEventTarget

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 handle.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>
  )
}

handle.frame

Reference to the component’s closest frame.
frame
FrameHandle
The closest ancestor Frame or the root frame.

handle.frames

Access to named frames in the current runtime tree.
frames
object
top
FrameHandle
The root frame for the current runtime tree.
get
(name: string) => FrameHandle | undefined
Retrieves a frame by name.

Mixins

Mixins provide reusable element enhancements through the mix prop.

createMixin()

Creates a custom mixin descriptor.
function createMixin<
  node extends EventTarget = Element,
  args extends unknown[] = [],
  props extends ElementProps = ElementProps,
>(type: MixinType<node, args, props>): MixinDescriptor<node, args, props>

type MixinType<node, args, props> = (
  handle: MixinHandle<node, props>,
  type: string,
) => ((...args: [...args, currentProps: props]) => void | RemixElement) | void
type
MixinType<node, args, props>
required
Mixin setup function that receives a handle and returns an optional runner function.

MixinHandle

The mixin handle provides lifecycle events and utilities:
MixinHandle
object
id
string
Stable identifier for the element.
frame
FrameHandle
The element’s frame.
element
MixinElement
Function for returning elements from mixins.
signal
AbortSignal
Signal aborted when element is removed.
update
() => Promise<AbortSignal>
Schedules an update for the parent component.
queueTask
(task: (node, signal) => void) => void
Queues a task to run after the next update.
addEventListener
(type: string, listener: Function, options?) => void
Listens to mixin lifecycle events: insert, reclaimed, remove, beforeRemove, beforeUpdate, commit.

Lifecycle Events

insert
MixinInsertEvent
Fired when the element is inserted into the DOM.
node
Element
The inserted DOM node.
parent
ParentNode
The parent node.
key
string | undefined
The element’s key prop.
reclaimed
MixinReclaimedEvent
Fired when an existing DOM node is reclaimed (reused) during reconciliation.
remove
Event
Fired when the element is removed from the DOM.
beforeRemove
MixinBeforeRemoveEvent
Fired before removal. Use event.persistNode() to delay removal for animations.
persistNode
(teardown: (signal) => void) => void
Register a teardown function to delay removal. The signal is aborted if removal is cancelled.
beforeUpdate
MixinUpdateEvent
Fired before each update during the reconciliation phase.
commit
MixinUpdateEvent
Fired after each update when DOM mutations are complete.

Example: Custom Tooltip Mixin

import { createMixin } from 'remix/component'

let tooltip = createMixin<HTMLElement, [text: string]>((handle) => {
  let tooltipEl: HTMLDivElement
  
  handle.addEventListener('insert', (event) => {
    tooltipEl = document.createElement('div')
    tooltipEl.className = 'tooltip'
    document.body.appendChild(tooltipEl)
    
    let node = event.node
    node.addEventListener('mouseenter', () => {
      tooltipEl.style.display = 'block'
    })
    node.addEventListener('mouseleave', () => {
      tooltipEl.style.display = 'none'
    })
  })
  
  handle.addEventListener('remove', () => {
    tooltipEl?.remove()
  })
  
  return (text) => {
    if (tooltipEl) {
      tooltipEl.textContent = text
    }
    return handle.element
  }
})

// Usage
<button mix={[tooltip('Click me!')]}>Hover</button>

on()

Attaches event listeners to elements with automatic cleanup.
function on<target extends Element, type extends EventType<target>>(
  type: type,
  handler: (event: Event, signal: AbortSignal) => void | Promise<void>,
  captureBoolean?: boolean,
): MixinDescriptor
type
string
required
Event type (e.g., ‘click’, ‘input’, ‘submit’).
handler
(event, signal) => void | Promise<void>
required
Event handler function. Receives the event and an AbortSignal that’s aborted when the handler is re-entered or the component is removed.
captureBoolean
boolean
Whether to use capture phase (default: false).

Example

import { on } from 'remix/component'

function SearchInput(handle: Handle) {
  let results: string[] = []
  let loading = false
  
  return () => (
    <input
      type="text"
      mix={[on('input', async (event, signal) => {
        let query = event.currentTarget.value
        loading = true
        handle.update()
        
        // Signal is aborted if user types again
        let response = await fetch(`/search?q=${query}`, { signal })
        if (signal.aborted) return
        
        results = await response.json()
        loading = false
        handle.update()
      })]}
    />
  )
}

ref()

Gets a reference to the DOM node after rendering.
function ref<node extends EventTarget>(
  callback: (node: node, signal: AbortSignal) => void
): MixinDescriptor

type RefCallback<node extends EventTarget> = (node: node, signal: AbortSignal) => void
callback
(node, signal) => void
required
Function called once when the element is first rendered. Receives the DOM node and an AbortSignal that’s aborted when the element is removed.

Example: Focus Management

import { ref } from 'remix/component'

function Form(handle: Handle) {
  let inputRef: HTMLInputElement
  
  return () => (
    <form>
      <input type="text" mix={[ref(node => inputRef = node)]} />
      <button mix={[on('click', () => inputRef.focus())]}>
        Focus Input
      </button>
    </form>
  )
}

Example: Cleanup with Signal

import { ref } from 'remix/component'

function ResizeTracker(handle: Handle) {
  let dimensions = { width: 0, height: 0 }
  
  return () => (
    <div mix={[ref((node, signal) => {
      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} × {dimensions.height}
    </div>
  )
}

css()

Applies CSS styles with support for pseudo-selectors, media queries, and descendant selectors.
function css(styles: CSSProperties): MixinDescriptor
styles
CSSProperties
required
CSS styles object with support for nested selectors, pseudo-selectors, and media queries.

Basic Example

import { css } from 'remix/component'

function Button() {
  return () => (
    <button mix={[css({
      color: 'white',
      backgroundColor: 'blue',
      padding: '12px 24px',
      borderRadius: '4px',
      border: 'none',
      cursor: 'pointer',
    })]}>
      Click me
    </button>
  )
}

Pseudo-selectors

<button mix={[css({
  backgroundColor: 'blue',
  '&:hover': {
    backgroundColor: 'darkblue',
    transform: 'translateY(-1px)',
  },
  '&:active': {
    backgroundColor: 'navy',
    transform: 'translateY(0)',
  },
  '&:disabled': {
    opacity: 0.5,
    cursor: 'not-allowed',
  },
})]}>
  Click me
</button>

Media Queries

<div mix={[css({
  display: 'grid',
  gridTemplateColumns: '1fr',
  gap: '16px',
  '@media (min-width: 768px)': {
    gridTemplateColumns: 'repeat(2, 1fr)',
  },
  '@media (min-width: 1024px)': {
    gridTemplateColumns: 'repeat(3, 1fr)',
  },
})]}>
  {/* content */}
</div>

Descendant Selectors

<nav mix={[css({
  display: 'flex',
  gap: '16px',
  '& a': {
    color: 'blue',
    textDecoration: 'none',
    padding: '8px 16px',
    '&:hover': {
      backgroundColor: '#f0f0f0',
    },
    '&[aria-current="page"]': {
      backgroundColor: 'blue',
      color: 'white',
    },
  },
})]}>
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>

Performance: css vs style

Use css for static styles and pseudo-selectors. Use style prop for dynamic values:
function ProgressBar(handle: Handle) {
  let progress = 0
  
  return () => (
    <div
      mix={[css({
        backgroundColor: 'blue', // Static
      })]}
      style={{
        width: `${progress}%`, // Dynamic
      }}
    >
      {progress}%
    </div>
  )
}

animateEntrance()

Animates element entrance with Web Animations API.
function animateEntrance(
  keyframes: Keyframe[],
  options?: KeyframeAnimationOptions
): MixinDescriptor
keyframes
Keyframe[]
required
Array of keyframes for the animation.
options
KeyframeAnimationOptions
Animation options (duration, easing, etc.).

Example

import { animateEntrance, spring } from 'remix/component'

<div mix={[animateEntrance(
  [
    { opacity: 0, transform: 'translateY(-20px)' },
    { opacity: 1, transform: 'translateY(0)' },
  ],
  { ...spring('bouncy') }
)]}>
  Animated content
</div>

animateExit()

Animates element exit before removal.
function animateExit(
  keyframes: Keyframe[],
  options?: KeyframeAnimationOptions
): MixinDescriptor

Example

import { animateExit, tween, easings } from 'remix/component'

{showModal && (
  <div mix={[animateExit(
    [
      { opacity: 1, transform: 'scale(1)' },
      { opacity: 0, transform: 'scale(0.9)' },
    ],
    { ...tween({ from: 0, to: 1, duration: 200, curve: easings.easeOut }) }
  )]}>
    Modal content
  </div>
)}

animateLayout()

Animates layout changes (position, size) when elements move or resize.
function animateLayout(options?: KeyframeAnimationOptions): MixinDescriptor
options
KeyframeAnimationOptions
Animation options for layout transitions.

Example

import { animateLayout, spring } from 'remix/component'

function ReorderableList(handle: Handle) {
  let items = ['A', 'B', 'C']
  
  return () => (
    <div>
      {items.map((item) => (
        <div key={item} mix={[animateLayout({ ...spring('smooth') })]}>
          {item}
        </div>
      ))}
    </div>
  )
}

Built-in Components

Fragment

Renders children without a wrapper element.
<Fragment>{children}</Fragment>
// or
<>{children}</>
children
RemixNode
Content to render.

Example

function List() {
  return () => (
    <>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </>
  )
}

Frame

Renders nested Remix applications within frames.
<Frame src={string} name?: string fallback?: RemixNode />
src
string
required
URL or path to the frame content.
name
string
Optional frame name for identification.
fallback
RemixNode
Content to show while frame is loading.

Animation Utilities

spring()

Creates a spring-based animation iterator with physics-based motion.
function spring(preset: SpringPreset, overrides?: SpringOptions): SpringIterator
function spring(options?: SpringOptions): SpringIterator

type SpringPreset = 'smooth' | 'snappy' | 'bouncy'

interface SpringOptions {
  duration?: number
  bounce?: number
  velocity?: number
}

interface SpringIterator extends IterableIterator<number> {
  duration: number
  easing: string
  toString(): string
}
preset
'smooth' | 'snappy' | 'bouncy'
Preset spring configuration.
  • smooth: duration 400ms, bounce -0.3 (overdamped)
  • snappy: duration 200ms, bounce 0 (critically damped)
  • bouncy: duration 400ms, bounce 0.3 (underdamped)
options
SpringOptions
duration
number
Perceptual duration in milliseconds (default: 300).
bounce
number
Bounce amount: -1 to ~0.95. Negative = overdamped, 0 = critical, positive = bouncy.
velocity
number
Initial velocity in units per second.
SpringIterator
object
duration
number
Time when spring settles to rest (milliseconds).
easing
string
CSS linear() easing function.
toString
() => string
Returns “duration ms easing” for CSS transitions.

CSS Transition Example

import { spring } from 'remix/component'

let s = spring('bouncy')
element.style.transition = `transform ${s}`
element.style.transform = 'translateX(100px)'

WAAPI Example

import { spring } from 'remix/component'

element.animate(
  [
    { transform: 'translateX(0)' },
    { transform: 'translateX(100px)' },
  ],
  { ...spring('smooth') }
)

JavaScript Animation Example

import { spring } from 'remix/component'

for (let position of spring({ duration: 500, bounce: 0.2 })) {
  element.style.transform = `translateX(${position * 100}px)`
}

tween()

Creates a tween-based animation iterator with easing curves.
function tween(options: TweenOptions): TweenIterator

interface TweenOptions {
  from: number
  to: number
  duration: number
  curve: BezierCurve
}

interface BezierCurve {
  x1: number
  y1: number
  x2: number
  y2: number
}

const easings = {
  linear: { x1: 0, y1: 0, x2: 1, y2: 1 },
  ease: { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 },
  easeIn: { x1: 0.42, y1: 0, x2: 1, y2: 1 },
  easeOut: { x1: 0, y1: 0, x2: 0.58, y2: 1 },
  easeInOut: { x1: 0.42, y1: 0, x2: 0.58, y2: 1 },
}
options
TweenOptions
required
from
number
required
Starting value (typically 0).
to
number
required
Ending value (typically 1).
duration
number
required
Duration in milliseconds.
curve
BezierCurve
required
Cubic bezier curve for easing.

Example

import { tween, easings } from 'remix/component'

element.animate(
  [
    { opacity: 0, transform: 'scale(0.9)' },
    { opacity: 1, transform: 'scale(1)' },
  ],
  { ...tween({ from: 0, to: 1, duration: 300, curve: easings.easeOut }) }
)

Utility Types and Classes

TypedEventTarget

A strongly-typed EventTarget subclass for custom events.
class TypedEventTarget<eventMap> extends EventTarget {
  addEventListener<type extends keyof eventMap>(
    type: type,
    listener: (event: eventMap[type]) => void,
    options?: AddEventListenerOptions,
  ): void
  
  removeEventListener<type extends keyof eventMap>(
    type: type,
    listener: (event: eventMap[type]) => void,
    options?: EventListenerOptions,
  ): void
}

Example

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>{props.children}</div>
  )
}

function Consumer(handle: Handle) {
  let theme = handle.context.get(ThemeProvider)
  
  handle.on(theme, {
    change() {
      handle.update()
    },
  })
  
  return () => <div>Theme: {theme.value}</div>
}

RemixNode

Type representing any renderable content.
type Renderable = RemixElement | string | number | bigint | boolean | null | undefined
type RemixNode = Renderable | RemixNode[]
Use for component children props:
function Layout() {
  return (props: { children: RemixNode }) => (
    <div>
      <header>My App</header>
      <main>{props.children}</main>
    </div>
  )
}

Props<T>

Extracts props for a specific HTML element type.
type Props<T extends keyof JSX.IntrinsicElements> = JSX.IntrinsicElements[T]

Example

interface MyButtonProps extends Props<"button"> {
  size: "sm" | "md" | "lg"
}

function MyButton() {
  return (props: MyButtonProps) => (
    <button className={`btn-${props.size}`} {...props}>
      {props.children}
    </button>
  )
}

JSX Configuration

TypeScript Config

Configure tsconfig.json for Remix JSX:
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "remix/component"
  }
}

Props Interface

All host elements accept these standard props:
key
string | number | bigint
Unique identifier for list reconciliation.
mix
MixValue
Array of mixin descriptors for element enhancements.
style
CSSProperties
Inline styles (use for dynamic values).
children
RemixNode
Child elements.
All standard HTML attributes are also supported.

Best Practices

State Management

Use minimal component state - only store what’s needed for rendering:
// ✅ Good: Derive computed values
function TodoList(handle: Handle) {
  let todos: Array<{ text: string; completed: boolean }> = []
  
  return () => {
    let completedCount = todos.filter(t => t.completed).length
    
    return (
      <div>
        {todos.map((todo, i) => <div key={i}>{todo.text}</div>)}
        <div>Completed: {completedCount}</div>
      </div>
    )
  }
}

// ❌ Avoid: Storing computed values
function TodoList(handle: Handle) {
  let todos: string[] = []
  let completedCount = 0 // Unnecessary state
  
  return () => (
    <div>
      {todos.map((todo, i) => <div key={i}>{todo}</div>)}
      <div>Completed: {completedCount}</div>
    </div>
  )
}

Event Handler Patterns

Do work in event handlers with minimal component state:
// ✅ Good: Work in handler, only store what renders need
function SearchResults(handle: Handle) {
  let results: string[] = []
  let loading = false
  
  return () => (
    <input mix={[on('input', async (event, signal) => {
      let query = event.currentTarget.value
      loading = true
      handle.update()
      
      let response = await fetch(`/search?q=${query}`, { signal })
      let data = await response.json()
      if (signal.aborted) return
      
      results = data.results
      loading = false
      handle.update()
    })]} />
  )
}

CSS Prop Usage

Use css for static styles, style for dynamic values:
function ProgressBar(handle: Handle) {
  let progress = 0
  
  return () => (
    <div
      mix={[css({
        height: '20px',
        backgroundColor: '#eee',
        borderRadius: '10px',
      })]}
      style={{
        width: `${progress}%`, // Dynamic
      }}
    />
  )
}

Controlled vs Uncontrolled Inputs

Only control inputs when programmatic control is needed:
// ✅ Uncontrolled: User is the only source of truth
function SearchForm(handle: Handle) {
  return () => (
    <form mix={[on('submit', (event) => {
      event.preventDefault()
      let formData = new FormData(event.currentTarget)
      let query = formData.get('query') as string
      // Use query
    })]}>
      <input name="query" />
      <button type="submit">Search</button>
    </form>
  )
}

// ✅ Controlled: Programmatic control needed
function SlugForm(handle: Handle) {
  let slug = ''
  let autoGenerate = false
  
  return () => (
    <input
      type="text"
      value={autoGenerate ? generateSlug() : slug}
      disabled={autoGenerate}
      mix={[on('input', (event) => {
        slug = event.currentTarget.value
        handle.update()
      })]}
    />
  )
}

Build docs developers (and LLMs) love