Latticereference

Runtime

The shared React runtime, slot composition, ref merging, and controllable state primitives every Lattice UI package builds on.

@lattice-ui/react-runtimeStable direction

Runtime 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];
Strict context for a primitive
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];
A controllable toggle
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.

PropTypeDescription
valueT | undefinedControlled value. When provided, the hook is controlled and never mutates internal state.
defaultValue *TInitial value used when running uncontrolled.
onChange(next: T) => voidCalled 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>;
Forwarding props onto a caller's element
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.
  • Event and Change tables — merged rather than replaced, the slot’s handler first, so a caller’s Activated and the primitive’s both fire.
  • Refs — the child’s ref, the forwarded ref and any ref in props are composed with composeRefs.

composeRefs

function composeRefs<T>(
...refs: Array<AnyRef<T> | undefined>
): (node: T | undefined) => void;
type AnyRef<T> = React.Ref<T> | React.ForwardedRef<T>;
Attaching multiple refs to one Instance
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.