Ref & RefMap
Ref and RefMap are holders you register as providers, so a service can reach a DOM element React owns. Ref holds one element; RefMap holds one per key.
Signatures
class Ref<T> {
current: T | null
readonly set: (value: T | null) => void
}class RefMap<T, K = string> {
set(key: K): (element: T | null) => void
get(key: K): T | null
all(): ReadonlyMap<K, T>
}set on a Ref is a bound property, so ref={search.set} is stable across renders. set(key) on a RefMap returns one cached callback per key, so a row does not detach and reattach its ref on every render.
Getting an element into a service
A subclass is the token, exactly as a PropsRef subclass is:
import { inject } from "@remodulo/container"
import { Ref, useResolve } from "@remodulo/react"
// A `Ref` subclass is the token, exactly as a `PropsRef` subclass is.
class OrderSearchRef extends Ref<HTMLInputElement> {}
class OrderSearchStore {
private readonly input = inject(OrderSearchRef)
onModuleMount(): void {
this.input.current?.focus()
}
}
function OrderSearch() {
const search = useResolve(OrderSearchRef)
// `set` is a stable callback ref that tolerates the `null` React passes on detach.
return <input ref={search.set} placeholder="Find an order" />
}OrderSearchStore injects OrderSearchRef and the view attaches the element to the same instance. React attaches refs before effects run and mount runs from an effect, so current is there by the time onModuleMount executes.
The setter is called with null on detach. Anything reading current outside mount checks for it, and teardown must not assume the element is still attached - capture what teardown needs at setup time.
For a list, register a RefMap and hand out one callback per key:
import { RefMap } from "@remodulo/react"
class OrderRowRefs extends RefMap<HTMLLIElement> {}
function OrderList({ orders }: { orders: readonly Order[] }) {
const rows = useResolve(OrderRowRefs)
return (
<ul>
{orders.map((order) => (
<li key={order.id} ref={rows.set(order.id)}>
{order.title}
</li>
))}
</ul>
)
}rows.get(id) returns the element or null; rows.all() is a read-only view of the live map, so an entry disappears from it when its row unmounts.