Skip to content

Component Overview

The component package is Remix V3's built-in UI layer. It is a lightweight JSX runtime --- not React, not Preact, not a virtual DOM diffing library. It produces real DOM nodes on the client and HTML strings or streams on the server, with a small footprint and no external dependencies.

What is a JSX runtime?

A JSX runtime is the library that turns <div>Hello</div> into JavaScript function calls. React is one JSX runtime; Remix has its own. You write the same JSX syntax, but the behavior underneath is different.

Key Features

FeatureWhat It Does
Server renderingrenderToString and renderToStream produce HTML on the server.
Client hydrationrun() attaches interactivity to server-rendered HTML without re-rendering.
Double-function componentsSetup logic runs once; the render function runs on every update.
MixinsComposable behaviors (css, on, ref, link, pressEvents) attached via the mix attribute.
AnimationsSpring physics, tweens, and layout animations built in.
FramesThe Frame component loads streaming server content into page regions.
ContextPass data through the component tree without prop drilling.
Client entriesMark components for selective hydration with clientEntry.

The Double-Function Pattern

Every component is a function that returns another function. The outer function is the setup phase (runs once per instance). The inner function is the render phase (runs each time the component updates).

A simple component with no interactivity:

tsx
function Greeting() {
  // Setup phase --- runs once
  return ({ name }: { name: string }) => (
    // Render phase --- runs on every update
    <p>Hello, {name}!</p>
  )
}

An interactive component adds a handle parameter. The handle provides update() to trigger re-renders:

tsx
function Counter() {
  return (handle) => {
    let count = 0

    return ({ initial = 0 }: { initial?: number }) => {
      count = initial

      return (
        <div>
          <p>Count: {count}</p>
          <button mix={on('click', () => {
            count++
            handle.update()
          })}>
            Increment
          </button>
        </div>
      )
    }
  }
}

What is a handle?

The handle is a runtime object that Remix gives to interactive components. It provides update() to schedule re-renders, signal for cleanup, context for the context API, and frame for frame navigation.

Minimal Example

Here is a complete server-rendered page with a styled heading:

tsx
import { css, Fragment } from 'remix/component'
import { renderToString } from 'remix/component/server'

function Page() {
  return ({ title }: { title: string }) => (
    <html>
      <head><title>{title}</title></head>
      <body>
        <h1 mix={css({ color: '#333', fontFamily: 'sans-serif' })}>
          {title}
        </h1>
        <p>Welcome to Remix V3.</p>
      </body>
    </html>
  )
}

let html = renderToString(<Page title="Home" />)

TypeScript Configuration

Set the JSX import source in your tsconfig.json so that .tsx files use Remix's runtime instead of React:

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "remix/component"
  }
}

Imports at a Glance

ts
// Components and mixins
import { css, on, ref, link, pressEvents, spring, tween, Fragment, Frame, navigate } from 'remix/component'

// Server rendering
import { renderToString, renderToStream } from 'remix/component/server'

// Client hydration
import { run, createRoot } from 'remix/component'

How It Differs from React

ConceptReactRemix Component
Component shapeSingle function returning JSXOuter function (setup) returning inner function (render)
StateuseState, useReducerLocal variables + handle.update()
Side effectsuseEffecthandle.queueTask(), handle.signal
StylingclassName + external CSScss() mixin with scoped styles
Event handlingonClick propon('click', handler) mixin
RefsuseRefref() mixin

TIP

If you are coming from React, the biggest shift is that there are no hooks. State is plain variables. You call handle.update() when you want to re-render.

Released under the MIT License.