references/create-mixins.md
references/create-mixins.mdBrowse 12 files
1,082 tokens
4,162 bytes
Token encoding: o200k_base
Snapshot 8beb6cd
← Back to SKILL.md
Creating Mixins
What This Covers
How to author your own reusable host-element behavior with createMixin. Read
this when the task involves:
- Combining multiple low-level events or DOM hooks into one semantic mixin
- Dispatching custom DOM events from a host node
- Encapsulating imperative DOM setup that several components share
- Typing custom events on
HTMLElementEventMapfor use withon(...)
For the built-in mixins most code should use, see mixins-styling-events.md.
Use createMixin from remix/ui to author reusable host-element behavior.
Most app code should use built-in core mixins (on, css, ref, link,
attrs) and animation mixins from remix/ui/animation. Create custom mixins
when combining multiple low-level events into one semantic event, or when the
pattern is reused across components.
Core Semantics
- A mixin handle is tied to one mounted host node lifecycle.
insertis the host-node availability point for imperative setup.removeis teardown for that same lifecycle.queueTaskruns post-commit and receives(node, signal)for mixins.- Mixin render functions should stay pure; side effects belong in
insert,remove, or queued work.
import { createMixin } from 'remix/ui'
let myMixin = createMixin<HTMLElement>((handle) => {
handle.addEventListener('insert', (event) => {
// event.node is the mounted host node
})
handle.addEventListener('remove', () => {
// Clean up listeners, timers, observers
})
return (props) => {
handle.queueTask((node) => {
// Post-commit work that needs the concrete host node
})
return <handle.element {...props} />
}
})
Patterns
Pure prop transform
let withTitle = createMixin(
(handle) => (title: string, props: { title?: string }) => (
<handle.element {...props} title={title} />
),
)
Lifecycle-managed imperative setup
let withFocus = createMixin<HTMLElement>((handle) => {
handle.addEventListener('insert', (event) => {
event.node.focus()
})
return (props) => <handle.element {...props} />
})
Custom Event Mixins
Create event mixins when you combine multiple low-level events into one semantic custom event that is reused across components.
- Namespace custom event names (
myapp:*) to avoid collisions. - Extend
Eventwith the data consumers need. - Declare the event on
HTMLElementEventMapfor type safety withon(...). - Dispatch from the host node inside the mixin.
import { createMixin, on } from 'remix/ui'
export let dragReleaseType = 'myapp:drag-release' as const
declare global {
interface HTMLElementEventMap {
[dragReleaseType]: DragReleaseEvent
}
}
export class DragReleaseEvent extends Event {
velocityX: number
velocityY: number
constructor(init: { velocityX: number; velocityY: number }) {
super(dragReleaseType, { bubbles: true, cancelable: true })
this.velocityX = init.velocityX
this.velocityY = init.velocityY
}
}
export let dragRelease = createMixin<HTMLElement>((handle) => {
let node: HTMLElement | undefined
let tracking = false
let velocityX = 0
let velocityY = 0
let lastX = 0
let lastY = 0
let lastT = 0
handle.addEventListener('insert', (event) => {
node = event.node
})
return () => (
<handle.element
mix={[
on('pointerdown', (event) => {
if (!event.isPrimary) return
tracking = true
lastX = event.clientX
lastY = event.clientY
lastT = event.timeStamp
node?.setPointerCapture(event.pointerId)
}),
on('pointermove', (event) => {
if (!tracking) return
let dt = Math.max(1, event.timeStamp - lastT)
velocityX = (event.clientX - lastX) / dt
velocityY = (event.clientY - lastY) / dt
lastX = event.clientX
lastY = event.clientY
lastT = event.timeStamp
}),
on('pointerup', () => {
if (!tracking) return
tracking = false
node?.dispatchEvent(new DragReleaseEvent({ velocityX, velocityY }))
}),
]}
/>
)
})
Consume it:
<div
mix={[
dragRelease(),
on(dragReleaseType, (event) => {
console.log('velocity:', event.velocityX, event.velocityY)
}),
]}
/>