Components & JSX
Remix V3 includes a lightweight component system for building HTML pages using JSX. This guide covers the component model in depth --- from basic syntax to server rendering, client-side hydration, and streaming.
If you are new to components, start with Part 3 of the tutorial. This guide goes deeper into every aspect of the system.
What is JSX?
JSX (JavaScript XML) is a syntax extension that lets you write HTML-like code inside JavaScript files. It looks like HTML but is actually JavaScript:
let element = <h1 class="title">Hello, world!</h1>This gets transformed by a compiler (TypeScript, Babel, or the runtime's built-in loader) into function calls:
let element = jsx('h1', { class: 'title', children: 'Hello, world!' })JSX is not HTML --- it is a convenient way to describe a tree of elements. The compiler transforms it into plain JavaScript at build time.
File extensions
Files containing JSX must use the .tsx extension (instead of .ts). This tells the compiler that the file contains JSX syntax and needs to be transformed.
Configuring JSX
Remix uses its own JSX runtime. Configure it in your tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "remix/component"
}
}The jsxImportSource tells TypeScript to use Remix's JSX runtime instead of React's. You do not need React installed.
Remix's Component Model
Remix's component model is fundamentally different from React:
- No virtual DOM --- On the server, components render directly to HTML strings or streams. There is no virtual DOM diffing.
- Lightweight client runtime --- On the client, Remix provides a small runtime for interactivity, not a full reconciliation engine.
- Double function pattern --- Components are functions that return render functions.
This design makes server rendering fast and keeps the client bundle small.
Component Structure
A Remix component is a function that returns a render function. The outer function creates the component; the inner function renders it with specific props:
function Greeting() {
return ({ name }: { name: string }) => (
<p>Hello, {name}!</p>
)
}Breaking this down:
function Greeting()--- The component function. Must start with a capital letter.return ({ name }) => ...--- Returns a render function that receives props.<p>Hello, {name}!</p>--- The JSX that becomes HTML.
Use the component like an HTML tag:
<Greeting name="World" />
// Renders: <p>Hello, World!</p>Why the Double Function?
The outer function is the setup phase --- it runs once when the component is created. The inner function is the render phase --- it runs each time the component needs to produce output. This separation supports:
- Server-side streaming (the setup phase can begin before all data is ready)
- Client-side updates (the render function can be called again with new props)
- Resource initialization (open connections, create state, etc. in the setup phase)
Props
Props (short for "properties") are the data you pass to a component, just like HTML attributes:
<BookCard title="Ash & Smoke" price={16.99} inStock={true} />Define props using a TypeScript interface:
interface BookCardProps {
title: string
price: number
inStock: boolean
genre?: string // Optional prop
}
function BookCard() {
return ({ title, price, inStock, genre = 'General' }: BookCardProps) => (
<div class="book-card">
<h3>{title}</h3>
<p>${price.toFixed(2)}</p>
<p>{genre}</p>
{inStock ? <span class="badge">In Stock</span> : null}
</div>
)
}Prop Types
Props can be any JavaScript value:
| Type | Example |
|---|---|
| String | <Comp name="Alice" /> |
| Number | <Comp count={42} /> |
| Boolean | <Comp active={true} /> or <Comp active /> |
| Object | <Comp user={{ name: 'Alice' }} /> |
| Array | <Comp items={[1, 2, 3]} /> |
| Function | <Comp onClick={() => console.log('clicked')} /> |
| JSX | <Comp icon={<Icon />} /> |
The children Prop
The children prop is special --- it represents content placed between the opening and closing tags:
<Layout title="Home">
<h1>Welcome!</h1>
<p>This is the children content.</p>
</Layout>In the component:
import type { RemixNode } from 'remix/component'
interface LayoutProps {
title: string
children?: RemixNode
}
function Layout() {
return ({ title, children }: LayoutProps) => (
<html>
<head><title>{title}</title></head>
<body>{children}</body>
</html>
)
}RemixNode is the type for anything renderable: strings, numbers, JSX elements, arrays of these, or null/undefined.
The Handle API
The Handle is an object available inside the setup phase of a component. It provides access to the component's lifecycle and context:
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>
)
}
}
}Three-level nesting for the Handle
When using the Handle API, the component has three levels of nesting:
- The component function (setup)
- A function that receives the handle
- The render function that receives props
The handle is only needed for interactive, client-side components.
handle.update()
Triggers a re-render of the component with the current state. This is how you update the UI in response to user interactions:
handle.update()handle.queueTask()
Schedules a task to run asynchronously. Useful for debouncing or batching updates:
handle.queueTask(() => {
// This runs after the current execution completes
fetchData().then((data) => {
items = data
handle.update()
})
})handle.signal
An AbortSignal that is aborted when the component is destroyed. Use it to cancel in-flight requests:
function DataLoader() {
return (handle) => {
let data = null
handle.queueTask(async () => {
let response = await fetch('/api/data', {
signal: handle.signal,
})
data = await response.json()
handle.update()
})
return () => (
<div>{data ? JSON.stringify(data) : 'Loading...'}</div>
)
}
}handle.id
A unique identifier for the component instance. Useful for generating unique DOM IDs:
return (handle) => {
return ({ label }: { label: string }) => (
<div>
<label for={`input-${handle.id}`}>{label}</label>
<input id={`input-${handle.id}`} />
</div>
)
}handle.context
Access the component tree's context (see Context API below).
Fragments
When you need to return multiple elements without a wrapping container, use a fragment:
function BookMeta() {
return ({ author, year }: { author: string; year: number }) => (
<>
<p>Author: {author}</p>
<p>Year: {year}</p>
</>
)
}The <>...</> syntax creates a fragment --- a grouping mechanism that does not produce any HTML element in the output.
Mixins
Mixins are reusable units of behavior and styling that you attach to elements using the mix attribute. They are one of Remix's most powerful features.
css() --- Inline Styles
import { css } from 'remix/component'
<div mix={css({ padding: '1rem', backgroundColor: '#f0f0f0' })}>
Styled content
</div>CSS properties use camelCase (e.g. backgroundColor, not background-color).
on() --- Event Listeners
import { on } from 'remix/component'
<button mix={on('click', () => {
console.log('Button clicked!')
})}>
Click Me
</button>Common events: 'click', 'input', 'change', 'submit', 'keydown', 'focus', 'blur', 'mouseenter', 'mouseleave'.
link() --- Navigation
import { link } from 'remix/component'
<a mix={link('/books')}>Browse Books</a>The link() mixin enables client-side navigation without a full page reload when the component system is hydrated on the client.
ref() --- DOM References
import { ref } from 'remix/component'
function FocusInput() {
return (handle) => {
let inputRef = ref<HTMLInputElement>()
return () => (
<div>
<input mix={inputRef} />
<button mix={on('click', () => {
inputRef.current?.focus()
})}>
Focus Input
</button>
</div>
)
}
}pressEvents --- Press Handling
import { pressEvents } from 'remix/component'
<button mix={pressEvents({
onPress: () => console.log('pressed'),
onPressStart: () => console.log('press start'),
onPressEnd: () => console.log('press end'),
})}>
Press Me
</button>pressEvents provides normalized press handling that works across mouse, touch, and keyboard interactions.
keysEvents --- Keyboard Shortcuts
import { keysEvents } from 'remix/component'
<div mix={keysEvents({
'Escape': () => closeModal(),
'Enter': () => submit(),
'Ctrl+S': () => save(),
})}>
Press Escape to close
</div>Combining Mixins
Pass an array of mixins to combine them:
<button mix={[
css({ padding: '0.5rem 1rem', background: 'blue', color: 'white' }),
on('click', handleClick),
pressEvents({ onPress: handlePress }),
]}>
Interactive Button
</button>Server Rendering
Remix provides two functions for rendering components to HTML on the server.
renderToString()
Renders the entire component tree to a single HTML string:
import { renderToString } from 'remix/component/server'
let html = renderToString(
<Layout title="Home">
<h1>Welcome</h1>
</Layout>,
)
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})renderToString is simple and works well for small to medium pages.
renderToStream()
Renders the component tree progressively, sending chunks of HTML as they become ready:
import { renderToStream } from 'remix/component/server'
let stream = renderToStream(
<Layout title="Home">
<h1>Welcome</h1>
<BookList />
</Layout>,
)
return new Response(stream, {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})renderToStream returns a ReadableStream that you can pass directly to a Response. The browser starts rendering HTML as soon as it arrives, which improves time-to-first-byte (TTFB) and perceived performance.
When to use streaming
Use renderToStream when:
- Your page loads data from a database or API (the browser can show the shell while data loads)
- Your page is large (users see content sooner)
- You want the best possible perceived performance
Use renderToString when:
- Your page is small and renders quickly
- You need the full HTML as a string (e.g., for caching or email)
Client-Side Hydration
Server-rendered HTML is static --- clicking buttons does nothing until JavaScript loads and hydrates the page. Hydration connects the server-rendered HTML to the client-side component runtime.
The run() Function
import { run } from 'remix/component/client'
run(document.getElementById('app')!, App)The run() function:
- Finds the server-rendered HTML in the given DOM element.
- Attaches event listeners and state management to the existing DOM.
- Makes the page interactive.
Client Entry
A typical client entry file:
import { run } from 'remix/component/client'
import { App } from './ui/app.tsx'
run(document.body, App)Include this script in your HTML document:
function Document() {
return ({ children }: { children?: RemixNode }) => (
<html>
<head>
<title>My App</title>
</head>
<body>
{children}
<script type="module" src="/app/client-entry.ts" />
</body>
</html>
)
}The Frame Component
The Frame component enables streaming partial server-rendered UI into a page. It acts as a boundary that can be loaded independently:
import { Frame } from 'remix/component'
function BookPage() {
return ({ bookId }: { bookId: string }) => (
<Layout>
<h1>Book Details</h1>
<Frame src={`/api/books/${bookId}/details`}>
<p>Loading book details...</p>
</Frame>
</Layout>
)
}The Frame component:
- Renders the
childrenimmediately as a placeholder - Fetches the content from
srcin the background - Replaces the placeholder with the fetched content when it arrives
This is useful for:
- Loading slow data without blocking the rest of the page
- Lazy-loading sections of a page
- Building dashboard-style layouts where each widget loads independently
Context API
The Context API lets you pass data through the component tree without explicitly threading it through every level of props:
import { createContext, useContext } from 'remix/component'
// Create a context with a default value
let ThemeContext = createContext('light')
// Provider component
function ThemeProvider() {
return ({ theme, children }: { theme: string; children?: RemixNode }) => (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
)
}
// Consumer component
function ThemedButton() {
return (handle) => {
let theme = handle.context.get(ThemeContext)
return ({ label }: { label: string }) => (
<button class={`btn-${theme}`}>{label}</button>
)
}
}
// Usage
<ThemeProvider theme="dark">
<ThemedButton label="Click Me" />
</ThemeProvider>The context value is available to all descendant components without passing it as a prop through intermediate components.
Animation
Remix includes animation utilities for smooth UI transitions:
spring()
import { spring } from 'remix/component'
let position = spring(0, { stiffness: 300, damping: 30 })
// Update the spring target
position.set(100)
// Use in styles
<div mix={css({ transform: `translateX(${position.value}px)` })}>
Animated
</div>tween()
import { tween, easings } from 'remix/component'
let opacity = tween(0, {
duration: 300,
easing: easings.easeInOut,
})
opacity.set(1)
<div mix={css({ opacity: opacity.value })}>
Fade In
</div>Easings
The easings object provides standard easing functions:
easings.lineareasings.easeIn,easings.easeOut,easings.easeInOuteasings.easeInCubic,easings.easeOutCubic,easings.easeInOutCubic- And more.
Rendering Lists
Use JavaScript's .map() to render arrays:
function BookList() {
return ({ books }: { books: Book[] }) => (
<ul>
{books.map((book) => (
<li>{book.title} by {book.author}</li>
))}
</ul>
)
}Conditional Rendering
Use ternary expressions or logical AND:
// Ternary: show one thing or another
{isLoggedIn ? <UserMenu /> : <LoginButton />}
// Logical AND: show something only if true
{errorMessage ? <p class="error">{errorMessage}</p> : null}
// Nullish values (null, undefined, false) render nothing
{showBanner && <Banner />}Related
- Tutorial: Building Pages --- Learn the basics step by step.
- Streaming --- Streaming HTML with
renderToStream. - Request & Response --- How rendered components become responses.
- Forms & Mutations --- Client-side form handling with mixins.