Sharing
Let users send a file from your app to any other app, and receive files other apps share into
yours. @symbiote-native/sharing wraps
expo-sharing so every
SymbioteNative adapter can reach it, not just React. Outgoing share is two free functions in a
shared core; the incoming half ships as useIncomingShare on React, Vue, Svelte and Solid, and
injectIncomingShare on Angular.
| OS platform | Support |
|---|---|
| iOS | ✅ live |
| Android | ✅ live |
| Framework adapter | Support |
|---|---|
| React | ✅ live |
| Vue | ✅ live |
| Angular | ✅ live |
| Svelte | ✅ live |
| Solid | ✅ live |
Scope: outgoing and incoming share
Section titled “Scope: outgoing and incoming share”expo-sharing has two halves, and both are ported to every adapter.
| Half | Upstream API | Here |
|---|---|---|
| Outgoing - hand a local file to another app | shareAsync, isAvailableAsync |
ported in full |
| Incoming - receive files other apps share into your app | useIncomingShare, getSharedPayloads, getResolvedSharedPayloadsAsync, clearSharedPayloads |
ported in full |
Receiving shared data
Section titled “Receiving shared data”import { useIncomingShare } from '@symbiote-native/sharing/react';
const { sharedPayloads, resolvedSharedPayloads, isResolving, error, clearSharedPayloads } = useIncomingShare();The payloads are read synchronously, resolved after mount, and re-read whenever the app returns
to the foreground. refreshSharePayloads() forces a re-read. Resolving a shared URL may need the
network. Vue returns a ComputedRef, Solid an Accessor, Svelte a { current } box, and
Angular’s injectIncomingShare() a Signal.
Installation
Section titled “Installation”npm install @symbiote-native/sharingScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --sharing (or
add --sharing in an existing app) installs and wires this for you — see
@symbiote-native/cli.
expo-sharing and expo-modules-core come along as regular dependencies, pinned to exact
versions — never install either yourself, and never add the expo meta-package to your project
(it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).
The package’s native-link.json asks symbiote-expo-link for the Android Gradle dependency and
module-map entry on every install. Nothing else is needed: opening the share sheet requires no iOS
permission, so there is no usage-description string, and the SharingFileProvider plus the
<queries> block Android needs on API 30+ ship inside expo-sharing’s own manifest and merge
into your app automatically.
All five adapters (React, Vue, Angular, Svelte, Solid) re-export the exact same functions; there’s no per-adapter hook/composable/service to reach for, since nothing here holds live state or a subscription.
import { useState } from 'react';import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/react';
export default function ShareReport({ fileUri }: { fileUri: string }) { const [error, setError] = useState<string | null>(null);
async function onShare() { if (!(await isAvailableAsync())) { setError('Sharing is not available on this device'); return; } await shareAsync(fileUri, { mimeType: 'application/pdf', dialogTitle: 'Send the report' }); }
return ( <view> <button title="Share the report" onPress={onShare} /> {error === null ? null : <text>{error}</text>} </view> );}<script setup lang="ts">import { ref } from 'vue';import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/vue';
const props = defineProps<{ fileUri: string }>();const error = ref<string | null>(null);
async function onShare() { if (!(await isAvailableAsync())) { error.value = 'Sharing is not available on this device'; return; } await shareAsync(props.fileUri, { mimeType: 'application/pdf', dialogTitle: 'Send the report', });}</script>
<template> <view> <button title="Share the report" @press="onShare" /> <text v-if="error">{{ error }}</text> </view></template>import { Component, input, signal } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` <view> <button title="Share the report" (press)="onShare()" /> @if (error(); as message) { <text>{{ message }}</text> } </view> `,})export class ShareReport { readonly fileUri = input.required<string>(); readonly error = signal<string | null>(null);
async onShare(): Promise<void> { if (!(await isAvailableAsync())) { this.error.set('Sharing is not available on this device'); return; } await shareAsync(this.fileUri(), { mimeType: 'application/pdf', dialogTitle: 'Send the report', }); }}There’s no per-instance service to inject() here — both functions are plain exports off the
core package, called straight from a template event binding.
<script lang="ts"> import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/svelte';
let { fileUri }: { fileUri: string } = $props(); let error = $state<string | null>(null);
async function onShare(): Promise<void> { if (!(await isAvailableAsync())) { error = 'Sharing is not available on this device'; return; } await shareAsync(fileUri, { mimeType: 'application/pdf', dialogTitle: 'Send the report' }); }</script>
<view> <button title="Share the report" onPress={onShare} /> {#if error} <text>{error}</text> {/if}</view>There’s no per-instance rune to reach for here either — both functions are plain exports off
the core package, called straight from onPress.
import { createSignal } from 'solid-js';import { isAvailableAsync, shareAsync } from '@symbiote-native/sharing/solid';
export function ShareReport(props: { fileUri: string }) { const [error, setError] = createSignal<string | null>(null);
async function onShare() { if (!(await isAvailableAsync())) { setError('Sharing is not available on this device'); return; } await shareAsync(props.fileUri, { mimeType: 'application/pdf', dialogTitle: 'Send the report' }); }
return ( <view> <button title="Share the report" onPress={onShare} /> {error() === null ? null : <text>{error()}</text>} </view> );}There’s no per-instance primitive here either: both functions are plain exports off the core
package, called straight from onPress. props.fileUri stays inline rather than destructured,
since a component body runs only once and a destructured value would freeze at mount.
On iPad
Section titled “On iPad”iOS presents the share sheet as a popover on iPad and needs somewhere to point it. Pass the rectangle of whatever the user tapped; without one the popover anchors to the bottom-center of the presenting view.
await shareAsync(fileUri, { anchor: { x: 40, y: 120, width: 1, height: 1 } });Functions
Section titled “Functions”| Signature | Description |
|---|---|
isAvailableAsync(): Promise<boolean> |
Whether the share sheet can be opened on this device. Resolves true on Android and iOS — it reports on the presence of the native module, not on any device capability |
shareAsync(url, options?): Promise<void> |
Opens the platform share sheet for the local file at url. Resolves once the sheet is dismissed, whether or not the user picked anything |
url must be a non-empty string pointing at a file the app can read — a file:// URI, or a path
from a file-system API. Anything else throws before the native call is made.
ISharingOptions
Section titled “ISharingOptions”| Field | Type | Description |
|---|---|---|
mimeType |
string | undefined |
MIME type of the file, deciding which apps the chooser offers. Guessed from the file name when omitted. Android only |
UTI |
string | undefined |
Uniform Type Identifier of the file. Accepted by the native options record but unread in expo-sharing@57.0.8; carried through for forward compatibility. iOS only |
dialogTitle |
string | undefined |
Title of the share dialog. Android renders it as the chooser header; iOS assigns it to the activity controller, where most share sheets ignore it |
anchor |
ISharingAnchor | undefined |
Rectangle the iPad popover points at. Ignored on iPhone and on Android. iOS only |
ISharingAnchor
Section titled “ISharingAnchor”| Field | Type | Description |
|---|---|---|
x |
number | undefined |
Horizontal offset in points, relative to the presenting view. Defaults to that view’s horizontal center |
y |
number | undefined |
Vertical offset in points, relative to the presenting view. Defaults to that view’s bottom edge |
width |
number | undefined |
Width of the anchor rectangle in points. Defaults to 0 |
height |
number | undefined |
Height of the anchor rectangle in points. Defaults to 0 |
urlhas to be local. A remotehttp(s)URL is not downloaded first — fetch it to a local file yourself, then share that file.- A resolved promise is not a delivery receipt. Neither platform reports which app the user picked, or whether they picked one at all: the promise resolves when the sheet closes. iOS resolves on every dismissal path, including “picked Print, then cancelled the print dialog”.
- Android runs one share at a time. Calling
shareAsyncagain while a chooser is still open rejects rather than queueing. - The iOS share extension is experimental upstream. It opens the main app target instead of handling the share in its own view controller, which Apple does not officially support and may stop working in a future iOS release.
- The share sheet can only be verified on a device or simulator. The headless test suite fakes the native module, so it proves argument marshalling and the error paths, not the sheet itself.
Common questions
Section titled “Common questions”Android says “Not allowed to read file under given URL” or “Failed to find configured root”.
Share a file that lives in the app’s own document or cache directory, for example one written with
the file system package. A path owned by another app, such as a
media library asset, can be rejected. Copy it into Paths.cache first, then share the copy.
It shares fine on iOS but the receiving app cannot open the file on Android. The same cause:
Android hands the other app a content URI built from the file, and only files in the app’s
directories are covered. Check url points inside them.
Can I share a web URL or plain text? Not with this function. shareAsync takes a local file.
To share a link, send it through the system’s text share (React Native’s Share module), or
download the content to a file first.
How do I know the user actually shared it? You cannot. The promise resolves when the sheet closes, whichever app was picked, or none.
Sources: Expo docs: Sharing, expo/expo#5933 can’t share file on Android, expo/expo#18887 Not allowed to read file under given URL, expo/expo#9222 sharing photos from MediaLibrary throws on Android.
How the wrapper works
Section titled “How the wrapper works”@symbiote-native/sharing ships zero React/Vue/Angular/Svelte/Solid logic — expo-sharing’s own
JS is hand-ported into this package’s core/, resolving the native module through
expo-modules-core’s requireNativeModule rather than the expo meta-package this project never
installs:
packages/sharing/src/core/ framework-agnostic: shareAsync, isAvailableAsync, the payload functions and the incoming-share store. native-module.ts resolves ExpoSharing via requireNativeModulereact/ @symbiote-native/sharing/react (core + useIncomingShare)vue/ @symbiote-native/sharing/vue (core + useIncomingShare)svelte/ @symbiote-native/sharing/svelte (core + useIncomingShare)angular/ @symbiote-native/sharing/angular (core + injectIncomingShare)solid/ @symbiote-native/sharing/solid (core + useIncomingShare)The store owns the refresh logic (payload comparison, the foreground resync, error capture) once,
and each adapter binds it with its own event-value primitive, so the lifecycle code is a few lines
per framework. The native code itself is never
vendored or copied — expo-modules-autolinking resolves it straight out of node_modules (see
the native setup guide).