Skip to content

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

ts
class Ref<T> {
    current: T | null
    readonly set: (value: T | null) => void
}
ts
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:

tsx
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:

tsx
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.

Where to go next

Guides

Reference

MIT licensed.