Skip to content

Remote Components

A remote component is a Vue component rendered in the sandboxed remote runtime and synchronized to host via channel updates.

Remote components describe what should be rendered. Host components (registered on host) decide how it is rendered in the real DOM.

  • createProvider() exists only on the host side.
  • On remote side, there is no provider registration step.
  • Remote side builds trees with:
    • defineRemoteComponent(...) for host-exposed components,
    • native tags (html, svg, mathml) for local structure.

Remote Vue code itself is not limited to scalar-only data. Inside remote components you can use normal Vue patterns (ref, computed, local functions, rich objects).

Serialization rules apply only at boundaries that are mirrored on host:

  1. defineRemoteComponent(...): props/attrs and emitted payloads that go to host components.
  2. Native html / svg / mathml nodes: attrs/props and children that are synchronized to host DOM. In practice, they behave like built-in mini host components.

For these boundaries, use postMessage-friendly data:

  • Scalars: string, number, boolean, null.
  • Arrays of serializable values.
  • Plain objects (POJO) with serializable nested values.
  • Combinations of the three above.

If a value is non-serializable, normalize it before passing across the boundary.

Use defineRemoteComponent to define a typed bridge to host components by name.

import {
defineRemoteComponent,
defineRemoteMethod,
} from '@omnicajs/vue-remote/remote'
const VButton = defineRemoteComponent('VButton')
const VInput = defineRemoteComponent('VInput', [
'update:value',
] as unknown as {
'update:value': (value: string) => true;
})
const VDialog = defineRemoteComponent('VDialog', {
methods: {
open: defineRemoteMethod<[id: string], boolean>(),
close: defineRemoteMethod<[], void>(),
},
})

The object form keeps emits, named slots, and host methods in one place:

import {
defineRemoteComponent,
defineRemoteMethod,
} from '@omnicajs/vue-remote/remote'
import { ref } from 'vue'
const VInput = defineRemoteComponent('VInput', {
emits: {
'update:value': (value: string) => value.length >= 0,
},
slots: ['prefix', 'suffix'],
methods: {
focus: defineRemoteMethod<[], void>(),
setSelectionRange: defineRemoteMethod<[start: number, end: number], void>(),
},
})
const input = ref<InstanceType<typeof VInput> | null>(null)
await input.value?.focus()
await input.value?.setSelectionRange(0, 2)

methods support three styles:

  • string[] for simple () => Promise<void> delegates.
  • validator objects for typed argument tuples with runtime validation before invoke.
  • defineRemoteMethod<Args, Result>(validator?) for typed arguments and typed async results.

When type is modeled as SchemaType<...>, methods become schema-aware:

import {
defineRemoteComponent,
defineRemoteMethod,
type SchemaType,
} from '@omnicajs/vue-remote/remote'
type VInputSchema = SchemaType<
'VInput',
{ modelValue: string },
{
focus: () => Promise<void>;
setSelectionRange: (start: number, end: number) => Promise<void>;
}
>
const VInputType = 'VInput' as VInputSchema
const VInput = defineRemoteComponent(VInputType, {
methods: {
focus: defineRemoteMethod<[], void>(),
setSelectionRange: defineRemoteMethod<[number, number], void>(),
// @ts-expect-error method is not declared in schema
scrollToTop: defineRemoteMethod<[], void>(),
},
})

In schema-aware mode:

  • method names are limited by the schema;
  • validator argument tuples must match the schema method;
  • incompatible defineRemoteMethod<Args, Result> signatures fail at compile time.

Legacy positional form is still supported:

const VCard = defineRemoteComponent('VCard', [], ['title'])

Migration note:

  • keep the positional form for fully legacy usage;
  • move to object form when you want typed ref methods;
  • add SchemaType only when you want compile-time enforcement of allowed method names and signatures.
import { createRemoteRenderer, createRemoteRoot, defineRemoteComponent } from '@omnicajs/vue-remote/remote'
import { defineComponent, h, ref } from 'vue'
const VButton = defineRemoteComponent('VButton')
const VInput = defineRemoteComponent('VInput', [
'update:value',
] as unknown as {
'update:value': (value: string) => true;
})
export async function mountRemote(channel: Parameters<typeof createRemoteRoot>[0]) {
const root = createRemoteRoot(channel, {
components: ['VButton', 'VInput'],
})
await root.mount()
const app = createRemoteRenderer(root).createApp(defineComponent({
setup () {
const text = ref('')
return () => h('section', { class: 'remote-form' }, [
h('h2', 'Remote form'),
h(VInput, {
value: text.value,
placeholder: 'Type here',
'onUpdate:value': (value: string) => text.value = value,
}),
h(VButton, { onClick: () => text.value = '' }, 'Clear'),
])
},
}))
app.mount(root)
}

Example: SVG + MathML + host component in one tree

Section titled “Example: SVG + MathML + host component in one tree”
import { defineComponent, h } from 'vue'
import { defineRemoteComponent } from '@omnicajs/vue-remote/remote'
const VCard = defineRemoteComponent('VCard', [], ['title'])
export default defineComponent({
setup () {
return () => h('article', { class: 'metrics' }, [
h('svg', { viewBox: '0 0 120 24', width: '100%', height: '24' }, [
h('path', { d: 'M0 18 L20 12 L40 14 L60 6 L80 10 L100 4 L120 8', stroke: 'currentColor', fill: 'none' }),
]),
h('math', { display: 'block' }, [
h('mrow', [h('mi', 'x'), h('mo', '='), h('mn', '42')]),
]),
h(VCard, {}, {
title: () => 'Summary',
default: () => 'Rendered by host, composed by remote.',
}),
])
},
})
<script setup lang="ts">
import { ref } from 'vue'
import { defineRemoteComponent } from '@omnicajs/vue-remote/remote'
const VButton = defineRemoteComponent('VButton')
const VInput = defineRemoteComponent('VInput', [
'update:value',
] as unknown as {
'update:value': (value: string) => true;
})
const text = ref('')
</script>
<template>
<section class="remote-form">
<h2>Remote form</h2>
<VInput
:value="text"
placeholder="Type here"
@update:value="text = $event"
/>
<VButton @click="text = ''">Clear</VButton>
</section>
</template>

SFC with native HTML/SVG/MathML + host components

Section titled “SFC with native HTML/SVG/MathML + host components”
<script setup lang="ts">
import { defineRemoteComponent } from '@omnicajs/vue-remote/remote'
const VCard = defineRemoteComponent('VCard', [], ['title'])
</script>
<template>
<article class="metrics">
<svg viewBox="0 0 120 24" width="100%" height="24">
<path d="M0 18 L20 12 L40 14 L60 6 L80 10 L100 4 L120 8" stroke="currentColor" fill="none" />
</svg>
<math display="block">
<mrow>
<mi>x</mi>
<mo>=</mo>
<mn>42</mn>
</mrow>
</math>
<VCard>
<template #title>Summary</template>
Rendered by host, composed by remote.
</VCard>
</article>
</template>

For native html / svg / mathml elements in remote templates, refs do not point to real browser DOM nodes. They point to remote tree nodes (proxy objects like RemoteComponent and related node types).

<script setup lang="ts">
import { onMounted, ref } from 'vue'
const el = ref<unknown>(null)
onMounted(() => {
// Not HTMLElement/SVGElement/MathMLElement.
// This is a remote tree node proxy.
console.log(el.value)
})
</script>
<template>
<div ref="el" />
</template>

Because of that, many familiar DOM APIs are not available in remote refs (getBoundingClientRect, classList, direct querySelector, etc.).

Most libraries that require real DOM access will not work on remote side for the same reason: the remote runtime manipulates proxy tree nodes, not actual browser elements.

If you need such a library:

  • integrate it on host side (inside a host component),
  • expose a narrow serializable API through props/events,
  • keep remote side focused on declarative state and intent.