@lattice-ui/react-runtimeStable directionRuntime is the foundation under every other Lattice UI package. It pins a single React and
React-Roblox runtime, and provides the composition utilities primitives reuse. A strict context
factory, ref-merging helpers, the Slot component behind asChild, and a controllable-state hook.
You rarely import it directly when consuming a primitive, but it is the first package you reach for when building one.
Import
import { React, ReactRoblox, Slot, createStrictContext, useControllableState, composeRefs, setRef, getElementRef, toRef,} from "@lattice-ui/react-runtime";API reference
React
import { React } from "@lattice-ui/react-runtime";A re-export of @rbxts/react as a default-style named binding. Every Lattice UI package imports
React through Runtime so the whole tree shares one instance — one context, hooks and reconciler
state. Import React from here rather than @rbxts/react to avoid a second copy.
ReactRoblox
import { ReactRoblox } from "@lattice-ui/react-runtime";A re-export of @rbxts/react-roblox, the host renderer behind roots and portals. The Layer
package’s Portal is built on ReactRoblox.createPortal.
createStrictContext
function createStrictContext<T>( name: string,): readonly [React.Provider<T | undefined>, () => T];const [AccordionProvider, useAccordion] = createStrictContext<AccordionState>("Accordion");
function Item() { // Throws a clear error if rendered outside <AccordionProvider>. const state = useAccordion(); return <frame />;}Creates a React context paired with a reader hook that throws when no provider is present rather
than returning undefined. name goes into the error message, so pass the primitive’s display
name. Returns [Provider, useContext], the hook returning a non-nullable T.
useControllableState
function useControllableState<T>(props: { value?: T; defaultValue: T; onChange?: (next: T) => void;}): readonly [T, (next: T | ((prev: T) => T)) => void];const [open, setOpen] = useControllableState({ value: props.open, defaultValue: props.defaultOpen ?? false, onChange: props.onOpenChange,});Powers the controlled/uncontrolled pattern across Lattice UI — the
open/defaultOpen/onOpenChange triad. With value provided, the state is controlled and
returned verbatim. With value undefined, the hook manages state from defaultValue. The setter
takes a value or an updater, calls onChange whenever the resolved value changes, and writes
internal state only when uncontrolled. Identical values are skipped.
| Prop | Type | Description |
|---|---|---|
| value | T | undefined | Controlled value. When provided, the hook is controlled and never mutates internal state. |
| defaultValue * | T | Initial value used when running uncontrolled. |
| onChange | (next: T) => void | Called whenever the resolved value changes, in both controlled and uncontrolled modes. |
Slot
const Slot: React.ForwardRefExoticComponent<SlotProps>;
type SlotProps = { children: React.ReactElement; ref?: React.ForwardedRef<Instance>;} & Record<string, unknown>;function Trigger(props: { asChild?: boolean; children: React.ReactElement }) { if (props.asChild) { return <Slot Event={{ Activated: () => open() }}>{props.children}</Slot>; } return <textbutton Event={{ Activated: () => open() }}>{props.children}</textbutton>;}Slot clones its single child element and merges the props it receives onto it instead of rendering
its own host instance. It is the engine behind asChild, letting a primitive hand off rendering
while still attaching its own behaviour.
Merging rules:
- Props — the slot’s own override the child’s of the same key.
EventandChangetables — merged rather than replaced, the slot’s handler first, so a caller’sActivatedand the primitive’s both fire.- Refs — the child’s ref, the forwarded ref and any
refin props are composed withcomposeRefs.
asChild requires a single valid React element as the child. Text, undefined, fragments or multiple children will not merge — wrap your content in one host element first.
composeRefs
function composeRefs<T>( ...refs: Array<AnyRef<T> | undefined>): (node: T | undefined) => void;
type AnyRef<T> = React.Ref<T> | React.ForwardedRef<T>;const mergedRef = composeRefs(localRef, props.forwardedRef);return <frame ref={mergedRef} />;Combines several refs into one callback ref: every supplied ref is updated with the node, callback
refs invoked and mutable ref objects assigned, with undefined entries skipped. Use it when a
component keeps its own ref while also forwarding the Instance to the caller.
setRef
function setRef<T>(ref: AnyRef<T> | undefined, value: T | undefined): void;Writes a value into a single ref, handling both forms, and a no-op when ref is undefined.
composeRefs is built on it.
getElementRef
function getElementRef<T>(child: React.ReactElement): AnyRef<T> | undefined;Extracts the ref from a React element, checking both element.props.ref and the legacy
element.ref, normalized through toRef. Slot uses it to recover a child’s ref before composing.
Returns undefined when there is none.
toRef
function toRef<T>(value: unknown): AnyRef<T> | undefined;Narrows an unknown value to a usable ref, a function or a table, returning undefined for anything
else. Useful when a ref arrives through an untyped prop bag.
Refs in @rbxts/react resolve to Roblox Instance objects, not DOM nodes. A freshly created useRef() may start with current absent rather than explicitly nil, which is why toRef treats any table as a valid mutable ref object.