useRef, DOM Access & getRef
useRef holds a mutable value that persists across renders without triggering re-renders when updated. In Dim it also powers cross-component DOM access via getRef.
Basic useRef
import { define, html, css, useRef, useEffect, useStyle } from "../core/dim.ts";
const SearchBox = (props, { useRef, useEffect, html, css, useStyle }) => {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
useStyle(css`
input { width: 100%; padding: 0.5rem; }
`);
return html`
<input
ref="${inputRef}"
type="search"
placeholder="Search…"
/>
`;
};
Bind a ref in templates with Lit’s ref="${ref}" directive on the element you need to access.
Default ref target
If you call useRef() without an initial value, current defaults to the component instance (the custom element). This supports legacy getRef lookups but is rarely what you want for DOM access — pass null explicitly:
const inputRef = useRef(null);
Mutable values without re-renders
const intervalRef = useRef(null);
const renderCountRef = useRef(0);
renderCountRef.current += 1; // does not re-render
useEffect(() => {
intervalRef.current = setInterval(tick, 1000);
return () => clearInterval(intervalRef.current);
}, []);
Use refs for timers, previous value tracking, and imperative handles — not UI state (use useState).
querySelector in sharedDependencies
Each component receives a shadow-root-scoped querySelector:
const Panel = (props, { querySelector, html }) => {
const focusFooter = () => {
querySelector(".footer-btn")?.focus();
};
return html`
<div>
<button class="footer-btn">Action</button>
<button @click="${focusFooter}">Focus footer</button>
</div>
`;
};
Prefer useRef when you own the element; use querySelector for ad-hoc queries inside your shadow tree.
getRef — cross-component access
getRef finds another custom element in your shadow tree and returns that component’s first useRef .current value:
const Parent = (props, { getRef, html }) => {
const readChildValue = () => {
const childInput = getRef("child-field");
console.log(childInput?.value);
};
return html`
<div>
<child-field></child-field>
<button @click="${readChildValue}">Read child</button>
</div>
`;
};
getRef accepts a CSS selector matching the child custom element tag. The child must expose a useRef whose .current points at the DOM node you need.
This is an escape hatch — prefer .props callbacks for parent ↔ child communication when possible.
Focus management pattern
const Modal = (props, { useRef, useEffect, html }) => {
const dialogRef = useRef(null);
useEffect(() => {
if (props.open) {
dialogRef.current?.focus();
}
}, [props.open]);
if (!props.open) return html``;
return html`
<div class="modal" ref="${dialogRef}" tabindex="-1">
${props.children}
</div>
`;
};
Pair with .props for open and onClose from the parent.
useRef vs useState
| useRef | useState | |
|---|---|---|
| Updates trigger re-render | No | Yes |
| Value stored on | .current | tuple index 0 |
| Survives renders | Yes | Yes |
| Good for | DOM nodes, timers, mutable counters | UI-visible data |
Conclusion
useRef covers imperative DOM work inside functional web components. querySelector and getRef extend that to shadow-tree queries and rare cross-element access.