Skip to main content

Component Overview

Remix Component is a minimal component system built on JavaScript and DOM primitives. Write components that render on the server, stream to the browser, and hydrate only where you need interactivity.

Features

  • JSX Runtime - Convenient JSX syntax for building UIs
  • Component State - State managed with plain JavaScript variables
  • Manual Updates - Explicit control over when components update via handle.update()
  • Real DOM Events - Events are real DOM events using the on() mixin
  • Inline CSS - CSS prop with pseudo-selectors and nested rules
  • Server Rendering - Stream full pages or fragments with renderToStream
  • Hydration - Mark interactive components with clientEntry and hydrate them on the client
  • Frames - Stream partial server UI into the page and reload without full page navigation

Quick Start

A simple counter component demonstrates the core concepts:
import { clientEntry, on, type Handle } from 'remix/component'

export let Counter = clientEntry(
  '/assets/counter.js#Counter',
  function Counter(handle: Handle, setup: number) {
    let count = setup

    return (props: { label: string }) => (
      <div>
        <span>
          {props.label}: {count}
        </span>
        <button
          mix={[
            on('click', () => {
              count++
              handle.update()
            }),
          ]}
        >
          +
        </button>
      </div>
    )
  },
)

Component Structure

All components follow a two-phase structure:

1. Setup Phase

Runs once when the component is first created. Use this for:
  • Initializing state from the setup prop
  • Creating instances (event emitters, observers, etc.)
  • Setting up cleanup with handle.signal
function MyComponent(handle: Handle, setup: number) {
  // Setup phase: runs once
  let count = setup
  let timer = setInterval(() => {
    count++
    handle.update()
  }, 1000)

  // Cleanup when component disconnects
  handle.signal.addEventListener('abort', () => {
    clearInterval(timer)
  })

  // Return render function
  return (props: { label: string }) => (
    <div>
      {props.label}: {count}
    </div>
  )
}

2. Render Phase

Runs on initial render and every update. The returned render function:
  • Receives props (not the setup prop)
  • Returns JSX to render
  • Is called after handle.update() completes
function MyComponent(handle: Handle, setup: number) {
  let count = setup

  return (props: { label: string }) => {
    // Render phase: runs on initial render and every update
    return (
      <div>
        {props.label}: {count}
      </div>
    )
  }
}

Setup Prop vs Props

The setup prop is special:
  • Only available in the setup phase - Passed as the second parameter to the component function
  • Not included in props - Automatically excluded from the props passed to the render function
  • Used for initialization - Initialize state that persists for the component’s lifetime
function Counter(handle: Handle, setup: number) {
  // setup = 5 (the setup prop value)
  let count = setup

  return (props: { label: string }) => {
    // props = { label: "Total" }
    // setup is NOT in props
    return (
      <div>
        {props.label}: {count}
      </div>
    )
  }
}

// Usage
<Counter setup={5} label="Total" />

The Handle API

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

Key Methods

  • handle.update() - Schedule an update and re-render the component
  • handle.queueTask(task) - Schedule work to run after the next update
  • handle.signal - AbortSignal aborted when the component disconnects
  • handle.id - Stable identifier for the component instance
  • handle.context - Get/set context values for ancestor/descendant communication

State Management

State is managed with plain JavaScript variables:
function TodoList(handle: Handle) {
  let todos: string[] = []

  return () => (
    <div>
      <input
        mix={[
          on('keydown', (event) => {
            if (event.key === 'Enter') {
              let input = event.currentTarget as HTMLInputElement
              todos.push(input.value)
              input.value = ''
              handle.update()
            }
          }),
        ]}
      />
      <ul>
        {todos.map((todo, i) => (
          <li key={i}>{todo}</li>
        ))}
      </ul>
    </div>
  )
}

Best Practices

  1. Use minimal state - Only store what’s needed for rendering
  2. Derive computed values - Calculate values in the render function instead of storing them
  3. Do work in event handlers - Keep transient state in event handler scope
  4. Don’t store input state you don’t need - Read from the DOM when possible
// Good: Derive computed values
function TodoList(handle: Handle) {
  let todos: Array<{ text: string; completed: boolean }> = []

  return () => {
    // Derive in render, don't store
    let completedCount = todos.filter((t) => t.completed).length

    return (
      <div>
        <div>Completed: {completedCount}</div>
        {todos.map((todo, i) => (
          <div key={i}>{todo.text}</div>
        ))}
      </div>
    )
  }
}

Component Types

Client Entry Components

Components marked with clientEntry render on the server and hydrate on the client:
import { clientEntry, on, type Handle } from 'remix/component'

export let Counter = clientEntry(
  '/assets/counter.js#Counter',
  function Counter(handle: Handle, setup: number) {
    let count = setup
    return (props: { label: string }) => (
      <div>
        <span>{props.label}: {count}</span>
        <button mix={[on('click', () => {
          count++
          handle.update()
        })]}>
          +
        </button>
      </div>
    )
  },
)

Server-Only Components

Components without clientEntry only render on the server:
function Header() {
  return (props: { title: string }) => (
    <header>
      <h1>{props.title}</h1>
    </header>
  )
}

Fragment Component

Use Fragment (or <>) to group elements without adding extra DOM nodes:
import { Fragment } from 'remix/component'

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

Next Steps

  • Rendering - Learn about component rendering and composition
  • State Management - Master state management patterns
  • Styling - Style components with the CSS prop
  • Events - Handle user interactions with events

Build docs developers (and LLMs) love