Expose and component refs
Sometimes a parent needs a child to act now, rather than when a prop changes: focus the search field, reset a counter, scroll a list to its end. A component lists what a parent may call with defineExpose, and the parent holds the component with a template ref, as it would hold an element (Template refs), and calls what it exposes from client code.
Exposing functions
defineExpose takes an object of the component's local functions, each in shorthand:
import { defineExpose, useTemplateRef } from "unframework";
export default function SearchField({ label }: { label: string }) {
const input = useTemplateRef<HTMLInputElement>();
function focus() {
input.value?.focus();
}
defineExpose({ focus });
return (
<label class="search">
{label}
<input ref={input} type="search" />
</label>
);
}
import { useTemplateRef } from "unframework";
import SearchField from "./SearchField.uf.tsx";
export default function Header() {
const search = useTemplateRef<{ focus(): void }>();
return (
<header class="header">
<SearchField ref={search} label="Find" />
<button type="button" onClick={() => search.value?.focus()}>
Search
</button>
</header>
);
}
The parent declares the ref with an object type of what it calls, useTemplateRef<{ focus(): void }>(), attaches it with ref={search}, and calls it in a handler, search.value?.focus(). The ref is null until the child is mounted, so the call reads it with ?.:
import { type CSSProperties, type Ref, useImperativeHandle, useRef } from "react";
export interface SearchFieldHandle {
focus(): void;
}
export interface SearchFieldAttributes {
className?: string;
style?: CSSProperties;
ref?: Ref<SearchFieldHandle>;
}
export default function SearchField({
label,
className,
style,
ref,
}: { label: string } & SearchFieldAttributes) {
const input = useRef<HTMLInputElement>(null);
function focus() {
input.current?.focus();
}
useImperativeHandle(ref, () => ({ focus }));
return (
<label className={cx("search", className)} style={style}>
{label}
<input ref={input} type="search" />
</label>
);
}
/** Joins class names, and the keys of an object's truthy entries, into one `className`. */
function cx(...parts: unknown[]): string {
const names: string[] = [];
for (const part of parts) {
if (typeof part === "string") {
if (part) names.push(part);
} else if (part && typeof part === "object") {
for (const [key, on] of Object.entries(part)) {
if (on) names.push(key);
}
}
}
return names.join(" ");
}
import SearchField from "./SearchField";
import { type CSSProperties, useRef } from "react";
export interface HeaderAttributes {
className?: string;
style?: CSSProperties;
}
export default function Header({ className, style }: HeaderAttributes) {
const search = useRef<{ focus(): void }>(null);
return (
<header className={cx("header", className)} style={style}>
<SearchField ref={search} label="Find" />
<button type="button" onClick={() => search.current?.focus()}>
Search
</button>
</header>
);
}
/** Joins class names, and the keys of an object's truthy entries, into one `className`. */
function cx(...parts: unknown[]): string {
const names: string[] = [];
for (const part of parts) {
if (typeof part === "string") {
if (part) names.push(part);
} else if (part && typeof part === "object") {
for (const [key, on] of Object.entries(part)) {
if (on) names.push(key);
}
}
}
return names.join(" ");
}
- React passes the ref as a prop,
ref, which the child fills withuseImperativeHandle. The child exports the handle's type,SearchFieldHandle, and the parent readssearch.current. - Vue writes
defineExpose, and the parentuseTemplateRef("search")withref="search". - Svelte exports each function from the component,
export function focus(), and the parent binds aletwithbind:this. - Angular makes each exposed function a public method, and the rest protected where the template reads them, private otherwise. The parent writes a template reference,
#search, and reads it withviewChild. - Solid passes a
refcallback, which the child calls with what it exposes, and the parent empties itsletwithonCleanup. - Qwik has no component instance to hold, so the parent passes a signal as
ref, and the child fills it with its$functions and empties it when it is removed, through an inline helper,exposeRef(expose, emulated). - Astro renders on the server only, so nothing calls what a component exposes: the ref and its calls are dropped (
expose, for information).
The argument is an object literal of local functions, by name: no state, no computed value and no function written in place (UF2030). Expose a function that reads or writes the state instead. A ref on a component that exposes nothing, or whose type names a function the component doesn't expose, is UF3046.
Several functions
A component may expose several functions, declared with function or as an arrow function:
import { defineExpose, ref } from "unframework";
export default function Counter({ start }: { start: number }) {
const count = ref(start);
function increment() {
count.value++;
}
const reset = () => {
count.value = start;
};
defineExpose({ increment, reset });
return <output class="count">{count.value}</output>;
}
The parent's ref type lists the ones it calls, useTemplateRef<{ increment(): void; reset(): void }>(), and each button calls one.
Call what a child exposes from client code only, as you'd read an element ref: a handler, a watcher's callback or a lifecycle hook (Template refs).
Fallthrough
A parent's class and style on a component fall through to the component's root element, merged with the root's own. defineOptions({ inheritAttrs: false }) turns it off. Every framework gets the same two attributes.
Context
provide and inject share a value with every descendant under an injection key, without passing it through each level. Each framework gets its own context, from React's createContext to Angular's InjectionToken.