How to: refs and attachments in Svelte
Svelte has no React-style ref prop and no Vue template ref. Symbiote’s own
components (View, Text, Pressable, …) forward no bind:this escape hatch
of their own either, so reaching the real native node from app code takes one
of two mechanisms depending on what you’re touching.
bind:this — a host tag you author yourself
Section titled “bind:this — a host tag you author yourself”When you hand-author the raw symbiote-view/symbiote-text host tag directly
(not going through the <View>/<Text> wrapper), bind:this gives you back a
real ShimElement. hostInstance() unwraps that into the imperative
host-instance API (measure, measureInWindow, setNativeProps, focus,
blur), and findNodeHandle() reads the committed native tag off it — the
tag only exists after the first commit, so read it from an $effect:
<script lang="ts"> import { findNodeHandle, hostInstance, type ShimElement } from '@symbiote-native/svelte';
let box = $state.raw<ShimElement | null>(null); let tag = $state<number | null>(null);
$effect(() => { if (box === null) return; tag = findNodeHandle(box); });
function onMeasure(): void { const instance = hostInstance(box); if (instance === undefined) return; instance.measure((x, y, width, height, pageX, pageY) => { // real on-screen frame }); }</script>
<symbiote-view p={{ testID: 'ref-box' }} bind:this={box}> ...</symbiote-view>Source: examples/svelte/components/RefApiDemo.svelte — the port of React’s
RefApiDemo.tsx, backing measure/setNativeProps/findNodeHandle.
Some components expose their own imperative surface without any of this:
ScrollView reads its own bind:this internally and exports plain functions
— scrollTo/scrollToEnd/flashScrollIndicators/getScrollNode — the
Svelte-5 twin of React’s useImperativeHandle/Vue’s expose(). Check the
Components API for a component’s own exported
surface before reaching for the raw host tag.
{@attach} — the route for everything else, and the recommended default
Section titled “{@attach} — the route for everything else, and the recommended default”The Svelte compiler rejects use:/transition:/class:/style: on a
component (“This type of directive is not valid on components”), and app
code here never authors a host element by hand for an ordinary component.
{@attach fn} is the one directive-shaped construct that does compile on a
component: it lands as a prop keyed by a real JS Symbol
(createAttachmentKey()), which rides through $props()/...rest untouched
— routeProp only ever walks string keys — and every Symbiote component that
spreads ...rest onto its own host tag forwards it there for free. From
there, the adapter’s own createAttachmentsSync() wires it to the real
committed node (adapters/svelte/src/runes/attachments.ts).
Concretely, that means {@attach} reaches the real host node on any ordinary
Symbiote component with zero extra code in that component:
<script> const logLifecycle = node => { console.log('attached', node); return () => console.log('detached', node); };</script>
<View testID="target" {@attach logLifecycle} />Wrapping a third-party Svelte action
Section titled “Wrapping a third-party Svelte action”fromAction from svelte/attachments converts an ordinary Svelte action
(init/update/destroy) into an attachment, so a library’s action-based API
still works against a Symbiote component:
<script> import { fromAction } from 'svelte/attachments'; import { View } from '@symbiote-native/svelte';
const action = (node, value) => ({ update: next => { /* react to a new value */ }, destroy: () => { /* teardown */ }, });</script>
<View testID="action-target" {@attach fromAction(action, () => someValue)} />Source: adapters/svelte/src/runes/attachments.smoke.test.ts, which
round-trips this exact shape end to end against a real compiled component.
Setting props on a <svelte:element> native leaf
Section titled “Setting props on a <svelte:element> native leaf”@symbiote-native/navigation‘s stack renders react-native-screens’ native
views (RNSScreen, RNSScreenStackHeaderConfig, RNSSearchBar, …) through
<svelte:element this={'RNSScreen'}>, because a literal <RNSScreen> in a
template would parse as a component reference, not an element. A dynamic
tag compiles through Svelte’s generic setAttribute codegen instead of the
custom-element p= property-set path, so p={bag} as a plain attribute
silently does nothing there. An attachment sidesteps that path entirely by
assigning the property from plain JS:
export function hostProps(props: Record<string, unknown>): (node: unknown) => void { return node => { if (!isShimElement(node)) return; node.p = props; };}<svelte:element this={'RNSScreen'} {@attach hostProps(plan.screenProps)}> ...</svelte:element>Which one?
Section titled “Which one?”| You have… | Use |
|---|---|
A raw symbiote-view/symbiote-text tag you wrote yourself |
bind:this + hostInstance()/findNodeHandle() |
A component with its own exported imperative functions (ScrollView.scrollTo, …) |
that component’s own bind:this |
An ordinary Symbiote component (View, Pressable, …) and you just need the node |
{@attach} |
| A third-party Svelte action | {@attach fromAction(action, () => arg)} |
A <svelte:element> native leaf (a capitalized, un-hyphenated native view name) |
{@attach} setting node.p |