Refs and DOM Access
Use .ref() for direct DOM access.
Basic Ref Usage
Section titled “Basic Ref Usage”The .ref() method takes a callback that runs when the element is mounted:
const AutofocusInput = ({ el }: Service) => () => { return el('input').ref((node) => { node.focus(); })();};Cleanup
Section titled “Cleanup”Return a cleanup function from the callback:
const ResizeObserved = ({ el, signal }: Service) => () => { const dimensions = signal({ width: 0, height: 0 });
return el('div').ref((node) => { const observer = new ResizeObserver((entries) => { const { width, height } = entries[0].contentRect; dimensions({ width, height }); });
observer.observe(node);
// Return cleanup function return () => observer.disconnect(); })( // ... children );};The cleanup runs when the element is removed from the DOM.
Passing Refs to Children
Section titled “Passing Refs to Children”If a parent needs access to a child’s DOM node, pass a callback:
const input = ({ el }: Service) => (props: { onRef?: (node: HTMLInputElement) => void }) => { return el('input').ref((node) => { props.onRef?.(node); })();};
const Form = (svc: Service) => () => { const { el } = svc; const Input = svc(input); // Wired component gets PascalCase let inputNode: HTMLInputElement | null = null;
return el('form')( Input({ onRef: (node) => { inputNode = node; } }), el('button').props({ onclick: () => inputNode?.focus() })('Focus Input') );};Or with signals for reactive access:
const Form = (svc: Service) => () => { const { el, signal } = svc; const Input = svc(input); const inputRef = signal<HTMLInputElement | null>(null);
return el('form')( Input({ onRef: (node) => inputRef(node) }), el('button').props({ onclick: () => inputRef()?.focus() })('Focus Input') );};Third-Party Library Integration
Section titled “Third-Party Library Integration”Use refs to integrate non-reactive libraries:
const Chart = ({ el, effect }: Service) => (props: { data: Readable<ChartData> }) => { return el('canvas').ref((canvas) => { // Initialize chart library const chart = new ChartLibrary(canvas, { data: props.data() });
// Update chart when data changes const disposeEffect = effect(() => { chart.update(props.data()); });
// Return cleanup return () => { disposeEffect(); chart.destroy(); }; })();};Multiple Refs
Section titled “Multiple Refs”For collections, store refs in a map:
const ScrollableList = ({ el, signal, map }: Service) => () => { const items = signal(['a', 'b', 'c', 'd', 'e']); const itemRefs = new Map<string, HTMLElement>();
const scrollToItem = (id: string) => { itemRefs.get(id)?.scrollIntoView({ behavior: 'smooth' }); };
return el('div')( el('div')( ...['a', 'b', 'c', 'd', 'e'].map(id => el('button').props({ onclick: () => scrollToItem(id) })(`Go to ${id}`) ) ), el('div').props({ style: 'height: 200px; overflow: auto' })( map(items, (item) => el('div').props({ style: 'height: 100px' }).ref((node) => { itemRefs.set(item(), node); return () => itemRefs.delete(item()); })(item) ) ) );};Anti-patterns
Section titled “Anti-patterns”Don’t Forget Cleanup
Section titled “Don’t Forget Cleanup”Refs that set up observers, listeners, or other subscriptions need cleanup to avoid memory leaks:
// ❌ WRONG - observer never disconnectedconst Measured = (svc: Service) => () => { const { el, signal } = svc; const size = signal({ width: 0, height: 0 });
return el('div').ref((node) => { const observer = new ResizeObserver((entries) => { const { width, height } = entries[0].contentRect; size({ width, height }); }); observer.observe(node); // Missing cleanup! Observer lives forever })();};// ✅ CORRECT - return cleanup functionconst Measured = (svc: Service) => () => { const { el, signal } = svc; const size = signal({ width: 0, height: 0 });
return el('div').ref((node) => { const observer = new ResizeObserver((entries) => { const { width, height } = entries[0].contentRect; size({ width, height }); }); observer.observe(node);
return () => observer.disconnect(); // Cleanup when unmounted })();};