Prop, Emit, and Expose Forwarding
Headless Prop, Emit, and Expose Forwarding; covers Reka UI useForwardProps, Reka UI useEmitAsProps, Reka UI useForwardPropsEmits.
| Package | @vizejs/ui/forwarding |
| Maturity | stable |
| Own the source | vize lib pull forwarding |
| Requires | — |
| Aliases | forward props, forward emits, forward expose, wrapper component helpers |
| Covers | Reka UI useForwardProps, Reka UI useEmitAsProps, Reka UI useForwardPropsEmits, Reka UI useForwardExpose |
API
toHandlerKey
Convert an event name to the handler prop key Vue uses.
function toHandlerKey<const Name extends string>(name: Name): EmitHandlerKey<Name>
useEmitAsProps
Turn selected events of the current component into onX props that re-emit them, for forwarding to a wrapped child. Instance-free: the event list is explicit (no getCurrentInstance()), and both names and payloads are inferred from the emit returned by defineEmits, so forwarding a misspelled event is a compile error.
function useEmitAsProps< Emit extends (...args: never[]) => unknown, const Names extends readonly EmitEventName<Emit>[], >(emit: Emit, events: Names): EmitsAsProps<Emit, Names[number]>
useForwardProps
Forward a wrapper's props to a child, dropping undefined values so the child's own defaults still apply. Note: Vue casts absent boolean props to false; declare such props with default: undefined on the wrapper to keep them forwardable as "unset".
function useForwardProps<const Props extends object>( props: Props, ): ComputedRef<ForwardedProps<Props>>
useForwardPropsEmits
useForwardProps and useEmitAsProps combined into one object to v-bind on the wrapped child.
function useForwardPropsEmits< const Props extends object, Emit extends (...args: never[]) => unknown, const Names extends readonly EmitEventName<Emit>[], >( props: Props, emit: Emit, events: Names, ): ComputedRef<ForwardedProps<Props> & EmitsAsProps<Emit, Names[number]>>
useForwardExpose
Re-expose a wrapped child's public API (and root element) from a wrapper. ts const { forwardRef, exposed } = useForwardExpose<{ focus: () => void }>(); defineExpose(exposed); Instance-free: the child is captured through a function ref and read lazily, so the exposed object always reflects the currently mounted child. Methods of a wrapped element are bound to it. Properties are undefined while nothing is mounted (always during SSR).
function useForwardExpose<Exposed extends object = {}>(): ForwardExposeControls<Exposed>
Behavior
Normative state x input -> outcome table for @vizejs/ui/forwarding. Every row
is proven by src/families/foundations/forwarding/forwarding.test.ts;
compile-only assertions live in
src/families/foundations/forwarding/forwarding.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| FW1 | event name | toHandlerKey(name) |
Vue's handler key (onUpdate:modelValue, onValueChange) |
converts event names to Vue handler keys |
| FW2 | selected events | useEmitAsProps(emit, names) |
frozen onX props re-emit with the original payload |
re-emits selected events with their payloads |
| FW3 | reactive props | useForwardProps(props) |
only defined props are forwarded, reactively, so child defaults survive | forwards only defined props so child defaults survive |
| FW4 | wrapper component | click, then call exposed method | props and emits reach the child and parent; the child's exposed API is reachable | wrappers forward props, emits, and the child's exposed API in the DOM |
| FW5 | forwarded element | before and after forwardRef(el) |
properties are undefined before mount; $el and bound element methods after |
forwarded expose binds element methods and is empty before mount |
| FW6 | server render | render twice, hydrate, click | byte-identical markup, no hydration diagnostics, interactive after hydration | renders identical wrapper markup on the server and hydrates cleanly |
| FW7 | public type API | defineEmits emit, props, expose |
event names/payloads inferred from emit; undeclared events and wrong payloads fail |
src/families/foundations/forwarding/forwarding.types.test-d.ts |
All helpers are instance-free (no getCurrentInstance()), so they work in
Vapor components and plain effect scopes.