remix-packages-documentation.md•18.8 kB
# @remix-run/events and @remix-run/dom Documentation
NOTE: this is unofficial and was generated by Cursor from within the Remix Jam demo using the following prompt:
> Please use the example code in @public/ and the types definitions to write documentation for @remix-run/events and @remix-run/dom and stick it in a markdown file in the root of this repo.
This documentation covers two core Remix 3 packages: `@remix-run/events` for declarative event handling and `@remix-run/dom` for creating reactive UI components.
## Table of Contents
- [@remix-run/events](#remix-runevents)
- [Core API](#core-api)
- [Target Helpers](#target-helpers)
- [Custom Event Types](#custom-event-types)
- [Interactions](#interactions)
- [Built-in Interactions](#built-in-interactions)
- [@remix-run/dom](#remix-rundom)
- [Component System](#component-system)
- [Root and Rendering](#root-and-rendering)
- [Element References](#element-references)
---
## @remix-run/events
A declarative event handling library that provides a clean, composable API for managing event listeners across any `EventTarget`.
### Core API
#### `events(target, descriptors)`
Adds events to a target and returns a cleanup function. This is the primary way to attach event listeners.
```ts
import { events, dom } from "@remix-run/events";
let cleanup = events(target, [
dom.click(event => {
console.log(event.target);
})
]);
// Later: cleanup all event listeners
cleanup();
```
**With EventTarget objects:**
```ts
import { events } from "@remix-run/events";
let drummer = new Drummer(80);
events(drummer, [
Drummer.change(() => this.update()),
Drummer.kick(() => {
console.log('kick!');
})
]);
```
**On document:**
```ts
import { events } from "@remix-run/events";
import { space, arrowUp, arrowDown } from "@remix-run/events/key";
events(document, [
space(() => {
drummer.toggle();
}),
arrowUp(() => {
drummer.setTempo(drummer.bpm + 1);
}),
arrowDown(() => {
drummer.setTempo(drummer.bpm - 1);
}),
]);
```
#### `events(target)`
Creates an event container that allows dynamic event management.
```ts
let container = events(target);
container.on([
dom.click(event => {
console.log("first handler");
})
]);
// Change events dynamically
container.on([
dom.mouseover(event => {
console.log("new handler");
})
]);
// Clean up all events
container.cleanup();
```
#### `bind(type, handler, options?)`
Attaches a raw string event to a target. Particularly useful for custom elements and web components.
```ts
import { events, bind } from "@remix-run/events";
events(target, [
bind("custom-event", event => {
console.log(event.target);
})
]);
```
**Type Signature:**
```ts
function bind<E extends Event = Event, ECurrentTarget = any, ETarget = any>(
type: string,
handler: EventHandler<E, ECurrentTarget, ETarget>,
options?: AddEventListenerOptions
): EventDescriptor<ECurrentTarget>
```
### Target Helpers
Pre-configured target proxies that provide type-safe access to native DOM events.
#### `dom` - HTMLElement Events
```ts
import { events, dom } from "@remix-run/events";
events(element, [
dom.click(event => { /* ... */ }),
dom.mouseover(event => { /* ... */ }),
dom.pointermove(event => { /* ... */ }),
// All HTMLElementEventMap events available
]);
```
#### `win` - Window Events
```ts
import { events, win } from "@remix-run/events";
events(window, [
win.resize(event => { /* ... */ }),
win.scroll(event => { /* ... */ }),
]);
```
#### `doc` - Document Events
```ts
import { events, doc } from "@remix-run/events";
events(document, [
doc.DOMContentLoaded(event => { /* ... */ }),
doc.visibilitychange(event => { /* ... */ }),
]);
```
#### `xhr` - XMLHttpRequest Events
```ts
import { events, xhr } from "@remix-run/events";
let request = new XMLHttpRequest();
events(request, [
xhr.load(event => { /* ... */ }),
xhr.error(event => { /* ... */ }),
]);
```
#### `ws` - WebSocket Events
```ts
import { events, ws } from "@remix-run/events";
let socket = new WebSocket('ws://localhost:8080');
events(socket, [
ws.message(event => { /* ... */ }),
ws.error(event => { /* ... */ }),
]);
```
### Custom Event Types
#### `createEventType(eventName)`
Creates a pair of functions: an event binder and an event creator. This is perfect for creating type-safe custom events on `EventTarget` subclasses.
```ts
import { createEventType } from '@remix-run/events';
// Create event types
let [kick, createKick] = createEventType('drum:kick');
let [snare, createSnare] = createEventType('drum:snare');
let [tempoChange, createTempoChange] = createEventType<number>('drum:tempo-change');
export class Drummer extends EventTarget {
// Export as static methods for easy access
static kick = kick;
static snare = snare;
static tempoChange = tempoChange;
playKick() {
// Dispatch the event
this.dispatchEvent(createKick());
}
setTempo(bpm: number) {
// Dispatch with detail
this.dispatchEvent(createTempoChange({ detail: bpm }));
}
}
// Usage
let drummer = new Drummer();
events(drummer, [
Drummer.kick(() => {
console.log('kick!');
}),
Drummer.tempoChange((event) => {
console.log('tempo changed to', event.detail);
})
]);
```
**Return Value:**
```ts
[
// Event binder function
<ECurrentTarget extends EventTarget = EventTarget>(
handler: EventHandler<CustomEvent<Detail>, ECurrentTarget>,
options?: AddEventListenerOptions
) => EventDescriptor<ECurrentTarget>,
// Event creator function
(...args: [init?: CustomEventInit<Detail>]) => CustomEvent<Detail>
]
```
### Interactions
Interactions are higher-level event patterns that combine multiple low-level events into a single semantic event.
#### `createInteraction(eventName, factory)`
Creates a custom interaction that encapsulates complex event patterns.
```ts
import { createInteraction, events } from "@remix-run/events";
import { press } from "@remix-run/events/press";
let tempoTap = createInteraction<HTMLElement, number>(
"tempo-tap",
({ target, dispatch }) => {
let taps: number[] = [];
let minTaps = 4;
let maxInterval = 2000;
let resetTimer: number;
let handleTap = () => {
let now = Date.now();
clearTimeout(resetTimer);
taps.push(now);
taps = taps.filter(tap => now - tap < maxInterval);
if (taps.length >= minTaps) {
let intervals = [];
for (let i = 1; i < taps.length; i++) {
intervals.push(taps[i] - taps[i - 1]);
}
let bpms = intervals.map(interval => 60000 / interval);
let avgBpm = Math.round(
bpms.reduce((sum, value) => sum + value, 0) / bpms.length,
);
dispatch({ detail: avgBpm });
}
resetTimer = window.setTimeout(() => {
taps = [];
}, maxInterval);
};
// Return cleanup function(s)
return events(target, [press(handleTap)]);
},
);
// Usage
<Button
on={[
tempoTap(event => {
drummer.play(event.detail);
}),
]}
>
SET TEMPO
</Button>
```
**Factory Context:**
```ts
{
dispatch: (options?: CustomEventInit<Detail>, originalEvent?: Event) => void;
target: Target;
}
```
**Factory Return:** `Cleanup | Cleanup[] | void`
### Built-in Interactions
#### Press Interactions
High-level pointer and keyboard interactions for button-like elements.
##### `press(handler, options?)`
A complete press interaction that handles both pointer and keyboard activation (Space/Enter keys).
```ts
import { press } from "@remix-run/events/press";
<Button
on={[
press(event => {
console.log('Button pressed!');
console.log('Input type:', event.detail.inputType); // 'pointer' | 'keyboard'
console.log('Original event:', event.detail.originalEvent);
})
]}
>
PLAY
</Button>
```
**Options:**
```ts
interface PressOptions {
hit?: number; // Hit detection threshold (default varies)
release?: number; // Release threshold (default varies)
delay?: number; // Long press delay (default varies)
}
```
**Event Detail:**
```ts
type PressEventDetail = {
originalEvent: PointerEvent;
target: Element;
inputType: 'pointer';
} | {
originalEvent: KeyboardEvent;
target: Element;
inputType: 'keyboard';
}
```
##### `pressDown(handler, options?)`
Fires when the press starts (pointer down or key down).
```ts
<Button on={[pressDown(event => { /* ... */ })]}>
Press Me
</Button>
```
- Sets `rmx-active="true"` attribute on the target during press
##### `pressUp(handler, options?)`
Fires when the press ends (pointer up or key up).
```ts
<Button on={[pressUp(event => { /* ... */ })]}>
Press Me
</Button>
```
- Removes `rmx-active` attribute from the target
##### `longPress(handler, options?)`
Fires after holding the press for a specified duration.
```ts
<Button on={[longPress(event => { /* ... */ }, { delay: 500 })]}>
Hold Me
</Button>
```
##### `outerPress(handler)` / `outerPressDown(handler)` / `outerPressUp(handler)`
Detects presses outside the target element. Useful for closing modals, dropdowns, etc.
```ts
import { outerPress } from "@remix-run/events/press";
<Modal on={[outerPress(() => closeModal())]}>
{/* Modal content */}
</Modal>
```
**Outer Press Event Detail:**
```ts
interface OuterPressEventDetail {
originalEvent: PointerEvent;
}
```
#### Key Interactions
Keyboard interactions that automatically prevent default browser behavior and follow WAI-ARIA practices.
All key interactions come from `@remix-run/events/key`:
```ts
import {
space,
enter,
escape,
arrowUp,
arrowDown,
arrowLeft,
arrowRight,
home,
end,
pageUp,
pageDown,
tab,
backspace,
del
} from "@remix-run/events/key";
```
**Example Usage:**
```ts
events(document, [
space(() => {
drummer.toggle();
}),
arrowUp(() => {
drummer.setTempo(drummer.bpm + 1);
}),
arrowDown(() => {
drummer.setTempo(drummer.bpm - 1);
}),
]);
```
**Available Key Interactions:**
- `space` - Space key (useful for triggering actions)
- `enter` - Enter key (useful for submitting forms, selecting items)
- `escape` - Escape key (useful for closing modals/menus)
- `arrowUp` / `arrowDown` / `arrowLeft` / `arrowRight` - Arrow keys
- `home` / `end` - Move to first/last item
- `pageUp` / `pageDown` - Page navigation
- `tab` - Tab key
- `backspace` / `del` - Deletion keys
**Event Detail:**
```ts
type KeyInteractionEvent = CustomEvent<{
originalEvent: KeyboardEvent;
}>
```
##### `createKeyInteraction(key)`
Create a custom key interaction for any key:
```ts
import { createKeyInteraction } from "@remix-run/events/key";
let ctrlS = createKeyInteraction('s');
events(document, [
ctrlS(event => {
if (event.detail.originalEvent.ctrlKey) {
// Save document
}
})
]);
```
---
## @remix-run/dom
A reactive component system that provides efficient DOM rendering and updates.
### Component System
Components in Remix 3 are defined as functions with `this: Remix.Handle`.
```ts
import type { Remix } from "@remix-run/dom";
function MyComponent(this: Remix.Handle) {
// Setup code runs once
let drummer = this.context.get(DrumMachine);
events(drummer, [
Drummer.change(() => this.update())
]);
// Return render function
return () => (
<div>
<h1>BPM: {drummer.bpm}</h1>
</div>
);
}
```
**Key Features:**
- Setup code runs once when component mounts
- Return a render function that runs on each render
- Access to lifecycle methods via `this.Handle`
#### Component Types
```ts
function DrumMachine(this: Remix.Handle<Drummer>) {
// Generic type provides context type
let drummer = new Drummer(80);
this.context.set(drummer);
return () => <Layout><Equalizer /></Layout>;
}
function Equalizer(this: Remix.Handle) {
// Access parent context
let drummer = this.context.get(DrumMachine);
return () => <div>{drummer.bpm}</div>;
}
```
### Remix.Handle API
#### `this.update()`
Trigger an update of the component.
```ts
events(drummer, [
Drummer.change(() => this.update())
]);
```
#### `this.context.set(value)`
Set a context value that child components can access.
```ts
function Parent(this: Remix.Handle<MyType>) {
let value = new MyType();
this.context.set(value);
return () => <Child />;
}
```
#### `this.context.get(Component)`
Get a context value from a parent component.
```ts
function Child(this: Remix.Handle) {
let value = this.context.get(Parent);
return () => <div>{value.data}</div>;
}
```
#### `this.queueTask(callback)`
Queue a task to run after the next update.
```ts
this.queueTask(() => {
// This runs after DOM updates
element.focus();
});
```
### Root and Rendering
#### `createRoot(element)`
Creates a root for rendering your application.
```ts
import { createRoot } from "@remix-run/dom";
createRoot(document.body).render(<DrumMachine />);
```
#### `createRangeRoot(range)`
Creates a root from a DOM Range object for more precise insertion points.
```ts
import { createRangeRoot } from "@remix-run/dom";
let range = document.createRange();
createRangeRoot(range).update(<App />);
```
### Element References
#### `connect(callback)`
Get a reference to an element when it connects to the DOM.
```ts
import { connect } from "@remix-run/dom";
function DrumControls(this: Remix.Handle) {
let stopButton: HTMLButtonElement;
let playButton: HTMLButtonElement;
return () => (
<>
<Button
on={[
connect(event => (playButton = event.currentTarget)),
press(() => {
drummer.play();
this.queueTask(() => {
stopButton.focus();
});
}),
]}
>
PLAY
</Button>
<Button
on={[
connect(event => (stopButton = event.currentTarget)),
press(() => {
drummer.stop();
this.queueTask(() => {
playButton.focus();
});
}),
]}
>
STOP
</Button>
</>
);
}
```
#### `disconnect(callback)`
Execute cleanup when an element is removed from the DOM.
```ts
import { disconnect } from "@remix-run/dom";
<div on={[disconnect(() => {
// Cleanup code
})]}>
Content
</div>
```
### Props Types
#### `Remix.Props<K>`
Get properly typed props for any HTML element or component.
```ts
import type { Remix } from "@remix-run/dom";
export function Button({ children, ...rest }: Remix.Props<"button">) {
return (
<button {...rest}>
{children}
</button>
);
}
interface TempoButtonProps extends Remix.Props<"button"> {
orientation: "up" | "down";
}
export function TempoButton({ orientation, css, ...rest }: TempoButtonProps) {
return (
<button {...rest} css={{ ...css }}>
<Triangle orientation={orientation} />
</button>
);
}
```
#### `Remix.RemixNode`
Type for Remix JSX children.
```ts
export function Layout({ children }: { children: Remix.RemixNode }) {
return (
<div>
{children}
</div>
);
}
```
### Special Props
#### `on` Prop
The `on` prop accepts an array of event descriptors and applies them to the element.
```ts
<Button
on={[
press(() => console.log('pressed')),
dom.mouseover(() => console.log('hover')),
]}
>
Click Me
</Button>
```
#### `css` Prop
Inline styles with TypeScript support.
```ts
<div
css={{
display: "flex",
background: "black",
borderRadius: "24px",
padding: "24px",
"&:hover": {
background: "#333",
}
}}
>
Content
</div>
```
### Complete Example
Here's a complete example combining both packages:
```ts
import { connect, createRoot, type Remix } from "@remix-run/dom";
import { events } from "@remix-run/events";
import { press } from "@remix-run/events/press";
import { space, arrowUp, arrowDown } from "@remix-run/events/key";
import { Drummer } from "./drummer";
function DrumMachine(this: Remix.Handle<Drummer>) {
let drummer = new Drummer(80);
// Listen to drummer events
events(drummer, [
Drummer.change(() => this.update())
]);
// Add keyboard shortcuts
events(document, [
space(() => drummer.toggle()),
arrowUp(() => drummer.setTempo(drummer.bpm + 1)),
arrowDown(() => drummer.setTempo(drummer.bpm - 1)),
]);
this.context.set(drummer);
return () => (
<div>
<h1>BPM: {drummer.bpm}</h1>
<Controls />
</div>
);
}
function Controls(this: Remix.Handle) {
let drummer = this.context.get(DrumMachine);
let playButton: HTMLButtonElement;
return () => (
<>
<button
disabled={drummer.isPlaying}
on={[
connect(event => (playButton = event.currentTarget)),
press(() => drummer.play())
]}
>
PLAY
</button>
<button
disabled={!drummer.isPlaying}
on={[press(() => drummer.stop())]}
>
STOP
</button>
</>
);
}
createRoot(document.body).update(<DrumMachine />);
```
---
## Type Definitions
### Event Handler
```ts
type EventHandler<E = Event, ECurrentTarget = any, ETarget = any> = (
event: EventWithTargets<E, ECurrentTarget, ETarget>,
signal: AbortSignal
) => any | Promise<any>;
```
### Event Descriptor
```ts
interface EventDescriptor<ECurrentTarget = any> {
type: string;
handler: EventHandler<any, ECurrentTarget>;
isCustom?: boolean;
options?: AddEventListenerOptions;
}
```
### Event Container
```ts
interface EventContainer {
on: (events: EventDescriptor | EventDescriptor[] | undefined) => void;
cleanup: () => void;
}
```
### Cleanup
```ts
type Cleanup = () => void;
```
---
## Best Practices
1. **Use interactions over raw DOM events** - Interactions like `press` handle both pointer and keyboard events automatically.
2. **Clean up event listeners** - Always store and call the cleanup function returned by `events()` when appropriate, though Remix components handle this automatically.
3. **Use context for shared state** - Pass data between components using `this.context.set()` and `this.context.get()`.
4. **Batch renders with queueTask** - Use `this.queueTask()` to run code after the next render, useful for focusing elements.
5. **Type your components** - Use `Remix.Handle<T>` to type your component's context.
6. **Prefer built-in key interactions** - Use pre-built key interactions like `space`, `enter`, etc. instead of raw keyboard event handlers for better accessibility.
7. **Create custom interactions for complex patterns** - When you have a complex event pattern that you reuse, create a custom interaction with `createInteraction`.
8. **Use createEventType for custom events** - When creating `EventTarget` subclasses, use `createEventType` to get type-safe event binding and dispatching.