usage of @remix-run/component
updated_at: 2026-02-05 00:00 +09:00 gist
usage of @remix-run/component
updated_at: 2026-02-05 00:00 +09:00 gist
| name | description |
|---|---|
remix-component |
usage of @remix-run/component |
@remix-run/componenthandle.update() triggers re-render@remix-run/interactiontype Component<Context = NoContext, Setup = undefined, Props = ElementProps> = (
handle: Handle<Context>,
setup: Setup,
) => (props: Props) => RemixNode;
type NoContext = Record<string, never>;
type RemixNode = Renderable | RemixNode[];
type Renderable =
| RemixElement
| string
| number
| bigint
| boolean
| null
| undefined;interface Handle<C = Record<string, never>> {
id: string;
update(task?: Task): void;
queueTask(task: Task): void;
signal: AbortSignal;
context: Context<C>;
frame: FrameHandle;
on<T extends EventTarget>(target: T, listeners: EventListeners<T>): void;
}
type Task = (signal: AbortSignal) => void;
interface Context<C> {
set(values: C): void;
get<ComponentType>(component: ComponentType): ContextFrom<ComponentType>;
}type Props<T extends keyof JSX.IntrinsicElements> = JSX.IntrinsicElements[T]
interface HostProps<eventTarget extends EventTarget> {
key?: any
children?: RemixNode
on?: EventListeners<eventTarget>
connect?: (node: eventTarget, signal: AbortSignal) => void
css?: StyleProps | string
animate?: AnimateProp
className?: string
// ... standard HTML/SVG/MathML attributes
}
## API Methods
### `handle.update(task?: Task)`
Triggers component re-render.
```tsx
function Counter(handle: Handle) {
let count = 0
return () => (
<button on={{ click: () => { count++; handle.update() } }}>
{count}
</button>
)
}Queues task after next render (for DOM operations).
handle.queueTask(() => element.scrollIntoView());Aborted when component disconnects.
function Clock(handle: Handle) {
let interval = setInterval(() => {
if (handle.signal.aborted) {
clearInterval(interval);
return;
}
handle.update();
}, 1000);
return () => <span>{new Date().toLocaleTimeString()}</span>;
}Ancestor/descendant communication.
// Provider
function App(handle: Handle<{ theme: string }>) {
handle.context.set({ theme: "dark" });
return () => (
<div>
<Header />
</div>
);
}
// Consumer
function Header(handle: Handle) {
let { theme } = handle.context.get(App);
return () => (
<header css={{ backgroundColor: theme === "dark" ? "#000" : "#fff" }} />
);
}Unique ID for component instance.
<label htmlFor={handle.id}>Name</label>
<input id={handle.id} />Register global event listeners (cleaned up on disconnect).
handle.on(window, { keydown: (e) => {/* ... */} });Inline styles with pseudo-selectors, nested rules, media queries.
css={{
color: 'white',
backgroundColor: 'blue',
'&:hover': { backgroundColor: 'darkblue' },
'&::before': { content: '""' },
'&[aria-selected="true"]': { border: '2px solid yellow' },
'.icon': { width: '16px' },
'@media (max-width: 768px)': { padding: '8px' },
}}Direct DOM node access on mount/unmount.
connect={(node: HTMLElement, signal: AbortSignal) => {
// node is DOM element
signal.addEventListener('abort', () => { /* cleanup */ })
}}Type-safe event listeners.
on={{
input: (event) => { /* event.currentTarget is typed */ },
click: (event) => { /* ... */ },
}}Enter/exit/layout animations.
animate={{
enter: { opacity: 0, duration: 200 },
exit: { opacity: 0, duration: 150 },
layout: true,
}}function Counter(handle: Handle, setup: number) {
// setup: passed once on creation
let count = setup;
// props: passed on every render
return (props: { label: string }) => (
<button
on={{
click: () => {
count++;
handle.update();
},
}}
>
{props.label} {count}
</button>
);
}
// Usage
<Counter setup={10} label="Count" />;function Component(handle: Handle) {
// Setup phase: runs once
let state = 0;
// Render phase: returns render function
return () => <div>{state}</div>;
}function Component(handle: Handle<{ value: string }>) {
handle.context.set({ value: "data" });
return () => <div />;
}import { createRangeRoot, createRoot } from "@remix-run/component";
// Standard root
createRoot(document.body).render(<App />);
// Range-based root
createRangeRoot(range).render(<App />);import { renderToString } from "@remix-run/component/server";
let html = await renderToString(<App />);<>
<li>Item 1</li>
<li>Item 2</li>
</>;Handle インターフェースcreateComponent: コンポーネントインスタンスの作成Fragment, Frame: 組み込みコンポーネント###Package Exports
import { createRoot, Fragment, Handle } from "@remix-run/component";
import { jsx, jsxDEV, jsxs } from "@remix-run/component/jsx-runtime";
import { renderToString } from "@remix-run/component/server";| Feature | Remix Component | React |
|---|---|---|
| State | Plain variables | useState hook |
| Updates | Manual handle.update() |
Automatic |
| Lifecycle | Setup/Render phases | Hooks |
| Events | Real DOM events | Synthetic events |
| CSS | css prop |
className/CSS-in-JS |
// ✅ 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>Completed: {completedCount}</div>;
};
}
// ❌ Don't store derived state
function TodoList(handle: Handle) {
let todos: string[] = [];
let completedCount = 0; // Unnecessary
return () => <div>Completed: {completedCount}</div>;
}connect={(node, signal) => {
let observer = new ResizeObserver(() => handle.update())
observer.observe(node)
signal.addEventListener('abort', () => observer.disconnect())
}}function KeyboardTracker(handle: Handle) {
let lastKey = "";
handle.on(window, {
keydown: (event) => {
lastKey = event.key;
handle.update();
},
});
return () => <div>Last key: {lastKey}</div>;
}function SearchInput(handle: Handle) {
let query = "";
return () => (
<input
type="text"
value={query}
on={{
input: (e) => {
query = e.currentTarget.value;
handle.update();
},
}}
/>
);
}function Form(handle: Handle) {
let showDetails = false;
let section: HTMLElement;
return () => (
<form>
<input
type="checkbox"
on={{
change: (e) => {
showDetails = e.currentTarget.checked;
handle.update();
if (showDetails) {
handle.queueTask(() => section.scrollIntoView());
}
},
}}
/>
{showDetails && <section connect={(node) => (section = node)} />}
</form>
);
}handle.update() to trigger re-renderconnect callback receives AbortSignal for cleanuphandle.signal aborts when component unmounts