Solid API
Primitives (view, text, pressable, text-input, scroll-view, …) are
plain lowercase intrinsic tags — write them directly in JSX, no import needed.
Import only what’s still a real component or module from
@symbiote-native/solid:
import { StyleSheet } from '@symbiote-native/solid';Solid drives the same engine as React/Vue/Svelte/Angular, through
solid-js/universal’s own createRenderer - the official, framework-shipped
seam for a non-DOM target. Compiled Solid JSX imports eleven functions
(createElement, insert, setProp, …) straight from
@symbiote-native/solid/renderer, and each one is a thin wrapper over the
engine’s mutation API. A Solid component body runs once; everything that
changes afterward has to cross as a signal or accessor, not a re-invocation.
See the reactivity guide before writing a
component that holds state.
Installation
Section titled “Installation”pnpm add @symbiote-native/solid react-native solid-jsreact-native and solid-js stay your app’s own top-level dependencies -
react-native is the Metro version anchor, so it has to be pinned at the app
root. @symbiote-native/engine is a peer dependency and installs alongside.
Two things Metro/Babel/tsc all need, none optional:
babel.config.js- add@symbiote-native/solid/babel-presettopresets, listed last (Babel applies presets in reverse array order, so last means “runs first”). It has to win the JSX before@react-native/babel-preset’s own React-JSX transform claims the same elements. Swap the order and you getcreateElementcalls no renderer in the app implements. The preset arrives pre-configured (generate: 'universal'+ the renderer’smoduleName); don’t override either option yourself.tsconfig.json-"jsx": "preserve","jsxImportSource": "@symbiote-native/solid". TypeScript resolves the JSX namespace from<jsxImportSource>/jsx-runtimeand nowhere else, even underpreserve. Without this the package’s own host-tag namespace (symbiote-view,symbiote-text, …) isn’t in scope and every JSX element fails to type check.
examples/solid is the reference for the full config, including
metro.config.js.
Component shape
Section titled “Component shape”- Events: real camelCase props (
onPress,onLongPress,onValueChange,onLayout) passed straight through the compiled JSX, same as Svelte’s shape, no directive and no kebab-case step. - Children: ordinary JSX, built once per component body.
pressableis a plain tag, so pressed state no longer arrives as a render-prop function child — wireonPressIn/onPressOutinto a signal instead. List components (FlatList,SectionList, …) still take a function child. See Children below. - Refs:
ref={callback}, a compile-time rewrite into a callback prop, not a runtime ref object. See Refs and handles below. - Styles:
style/classprops, resolved through the same style registry every other adapter’sclass/className/:classpath shares. - Two-way binding has no dedicated syntax:
TextInput/Switch/Slidertake the explicitvalue/onValueChangepair; wire it to a signal yourself (value={text()} onValueChange={setText}).
Common events
Section titled “Common events”<pressable onPress={onTap} onLongPress={onHold} /><switch value={enabled()} onValueChange={setEnabled} /><text-input value={text()} onValueChange={(next, event) => onChange(event)}/><view onLayout={onLayout} />TextInput’s onValueChange fires (text, event), the same merged shape
React and Svelte use, unlike Angular, which needs a second output because an
EventEmitter only carries one value.
Children
Section titled “Children”pressable is a plain tag, so there’s no component body left to call a
render-prop function child against — wire the two press events into a signal
instead; reading it inside the JSX stays reactive the same way reading an
Accessor would:
import { createSignal } from 'solid-js';
const [pressed, setPressed] = createSignal(false);
<pressable onPress={onTap} onPressIn={() => setPressed(true)} onPressOut={() => setPressed(false)}> <text>{pressed() ? 'Release' : 'Press me'}</text></pressable>;List components (FlatList, SectionList, …) still take a genuine render
function per cell — see Solid
reactivity
for that pattern and the device bug it was built to avoid.
List renderers follow the same principle: the data and native list math are
shared with every other adapter, while the rendered cell is ordinary Solid
JSX (<For>/<Index>) rather than a React node, Vue slot, or Svelte
snippet.
Refs and handles
Section titled “Refs and handles”import { createSignal } from 'solid-js';import type { IHostInstance } from '@symbiote-native/solid';
const [input, setInput] = createSignal<IHostInstance | null>(null);
<text-input ref={setInput} value="" onValueChange={() => {}} />;Solid’s ref={el} is a compile-time construct: the compiler rewrites it
into a callback prop (createComponent(TextInput, { ref(node) { ... } })), so
by the time a component body reads props.ref it is already a function.
There is no RefObject/template-ref wrapper to allocate: the callback is
called once with the committed host node, which carries the grafted
imperative API (measure, measureInWindow, setNativeProps, focus,
blur) exactly like React’s getPublicInstance. Holding it in a signal (as
above) is the idiomatic way to keep it across the component’s lifetime; a
plain let variable also works if nothing needs to react to the ref
resolving.
A component with its own exported imperative surface
(ScrollView.scrollTo/scrollToEnd/flashScrollIndicators, TextInput’s
focus/blur/clear) exposes that surface directly on the ref, same as
every other adapter.
Runtime modules
Section titled “Runtime modules”@symbiote-native/solid re-exports the same runtime utilities as the other
adapters, so app code keeps one import root: Platform, StyleSheet,
Dimensions, PixelRatio, PlatformColor, DynamicColorIOS, Alert,
Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings,
I18nManager, Appearance, AppState, Keyboard, BackHandler,
PermissionsAndroid, LayoutAnimation, InteractionManager,
AccessibilityInfo, findNodeHandle, and dlog/isDebug for diagnostic
logging.
Two reactive reads are primitives, not hooks: Solid’s ecosystem term for
a composable reactive function, and why this adapter’s lifecycle bucket is
named primitives/.
import { createColorScheme, createWindowDimensions,} from '@symbiote-native/solid';
const scheme = createColorScheme();const dimensions = createWindowDimensions();
<text> {dimensions().width}×{dimensions().height} · {scheme()}</text>;Each returns an accessor, never a bare value: a Solid component body
runs once, so a returned snapshot would freeze at whatever the app booted
with. Named create*, not use*: Solid reserves use* for consuming
something that already exists (useContext, useTransition); a function
that owns its own subscription is create*. useNavigation/useRoute from
@symbiote-native/navigation are the use* counterpart: they consume an
existing router rather than creating one.
On Android, Keyboard and Settings do nothing until you also install
@symbiote-native/android:
keyboardDidShow/keyboardDidHide never fire, because Android’s only stock
keyboard-event source is ReactRootView’s layout listener and the bridgeless
surface SymbioteNative mounts never triggers it, and Settings reads back
null, because RN’s Settings has no stock Android implementation at all.
Animations and gestures
Section titled “Animations and gestures”Animated (both the JS and native driver, plus the Animated.View/
Animated.Text/Animated.Image/Animated.ScrollView/Animated.FlatList/
Animated.SectionList components) and PanResponder are re-exported from
@symbiote-native/solid. See the Animations guide
for the full surface. Solid’s JSX takes the dotted form directly
(<Animated.View> compiles to createComponent(Animated.View, ...)), unlike
Angular, whose AOT compiler needs a named import instead.
Portals (Portal)
Section titled “Portals (Portal)”import { createSignal } from 'solid-js';import { Portal, Show, type ISymbioteNode } from '@symbiote-native/solid';
const [overlayHost, setOverlayHost] = createSignal<ISymbioteNode | null>(null);
<view ref={node => setOverlayHost(node)} />;<Show when={overlayHost()}> {host => ( <Portal mount={host()}> <text>Rendered under overlayHost, not here</text> </Portal> )}</Show>;Portal relocates its children under a different node than its own call
site: the same primitive as React’s createPortal, Angular’s
PortalDirective/PortalOutletDirective pair, Vue’s Teleport, and
Svelte’s own Portal. mount takes an already-mounted host node (or a
surface, from AppRegistry), never a selector string: Solid’s own
solid-js/web Portal
resolves a DOM container the same way ours resolves a ref, so the spelling
matches what a Solid author already knows. Same v1 scope as React’s and
Angular’s: the target must live in the same surface as the <Portal> call
site. Gate the Portal behind <Show> until the target ref resolves -
reading an unset mount throws immediately rather than silently corrupting
the tree.
Cross-surface content (createTunnel)
Section titled “Cross-surface content (createTunnel)”import { createTunnel, Show } from '@symbiote-native/solid';
const overlayTunnel = createTunnel(); // module-level singleton, importable from both surfaces
// inside the surface that should paint the content<overlayTunnel.Out />;
// inside any other component, in any surface<Show when={toastVisible()}> <overlayTunnel.In> <text>Toast</text> </overlayTunnel.In></Show>;Portal only reaches a target in the same surface. createTunnel is for two
independently mount()-ed surfaces that share no Fabric tree at all. In
and Out are per-call components closed over the tunnel’s own registry.
Solid components are ordinary functions, so createTunnel() mints a fresh
pair on every call, the same shape React’s and Vue’s versions use. In
registers its content wherever it renders and paints nothing itself; Out
renders everything currently tunneled in, in registration order, wherever it
is actually mounted.
App entry point (AppRegistry)
Section titled “App entry point (AppRegistry)”import { AppRegistry } from '@symbiote-native/solid';
AppRegistry.registerComponent('MyApp', () => App);@symbiote-native/solid exports the same AppRegistry/setHostRegistrar
entry point described in the Core API.
setWrapperComponentProvider is fully supported here, unlike Svelte: a Solid
component is an ordinary function, so wrapping composes exactly the way
compiled JSX already builds children:
createComponent(Wrapper, { get children() { return renderRoot(); } }),
with no extra design needed:
import type { Component, JSX } from 'solid-js';import { AppRegistry } from '@symbiote-native/solid';
const ThemeWrapper: Component<{ children?: JSX.Element }> = props => ( <ThemeProvider>{props.children}</ThemeProvider>);
AppRegistry.setWrapperComponentProvider(() => ThemeWrapper);AppRegistry.registerComponent('MyApp', () => App);Boundary
Section titled “Boundary”Do not pass React component packages to the Solid adapter. A third-party React Native package that ships a JavaScript React component still uses the React dispatcher internally. Non-React adapters need native-view wrappers instead. See the Slider package for the reference implementation, shipping on React, Vue, Angular, Svelte, and Solid alike.