Refs
Sometimes you need the actual element on the page — to focus an input, measure something, scroll it, or hand it to a library that wants a real node (a chart, an editor, a map). A ref gives you that node.
import { createRef } from "@ramonda/core";
export class SearchBox extends Component {
private input = createRef<HTMLInputElement>();
focusIt() {
this.input.current?.focus();
}
render() {
return <input ref={this.input} type="search" aria-label="Search" />;
}
}
0 characters
current is empty until the element exists
this.input.current is null until the element is on the page. So reach for it from
an event handler or from @mounted — never from render(), where the element doesn't
exist yet.
@mounted
ready() {
this.input.current?.focus();
}
Not for changing what's on screen
A ref is for things state can't express. Don't use it to read or rewrite what the component shows — that is state's job, and the next render would overwrite whatever you changed by hand anyway.
A ref goes on an element, not on a component
A component may render one element, several, or none, so <Card ref={…} /> has no answer
to which element it would mean. Put the ref inside the component, on the element that
should carry it:
import { createRef } from "@ramonda/core";
class Card extends Component {
private box = createRef<HTMLDivElement>();
render() {
return <div ref={this.box}>…</div>;
}
}
If the caller is the one who needs it, take the ref as an ordinary prop and place it where you know it belongs:
import type { RefTarget } from "@ramonda/core";
class Card extends Component<{ box?: RefTarget<HTMLDivElement> }> {
render() {
return <div ref={this.props.box}>…</div>;
}
}
Callback refs
ref also takes a function, called with the node when it appears and null when it
goes away:
import { Component, createRef } from "@ramonda/core";
class Watched extends Component {
private observer = new IntersectionObserver(() => {});
private box = createRef<HTMLDivElement>((node) => {
if (node) this.observer.observe(node);
});
render() {
return <div ref={this.box} />;
}
}
The callback goes to createRef, not into the JSX. A ref prop takes a ref — something
that can receive the element — and a function written in the attribute would also be a new
one on every render, which
RMD020
reports.
A ref is cleared when its element is removed, so it can't keep a detached node alive.
Ref, RefTarget and RefCallback
createRef is the only one of these you call. The other two appear when a type is written by hand
— a field annotation, a function that takes a ref.
Call createRef where an identity belongs — a field, or anywhere that runs once. From a
render(), a @compute, a @memoized member or a hook's props callback it answers a new object
every pass: the child is handed a changed ref on every render, and the ref you meant to read is
replaced before you can. RMD061 reports it, and
ref-built-where-it-cannot-be-kept says the same before
it runs.
Ref<T> is what createRef<T>() hands back: a current that is the node or null, and the
setCurrent the framework calls. Annotating a field with it is the usual reason to name it —
private input: Ref<HTMLInputElement> = createRef().
RefTarget<T> is what a ref prop accepts, and it is deliberately smaller than Ref<T>: only
setCurrent. Ref<T> holds a mutable current, which makes it invariant — a
createRef<HTMLElement>() would be refused on a <p>, because Ref<HTMLElement> is not a
Ref<HTMLParagraphElement>. Asking only for the setter is the direction that is safe: a ref that
can hold any element can certainly hold this one. Take RefTarget<T> when you write a component
that forwards a ref onward.
RefCallback<T> is the function createRef takes — (current: T | null) => void.
Next
- Hooks — state and lifecycle with no element at all.