Three tools, three jobs
| Option | Availability | Handles | Does not |
|---|---|---|---|
| Share from react-native | Built in, no install | Text and, on iOS, a link | Files, images, specific apps |
| expo-sharing | Expo Go supported | Any local file URI with a mime type | Plain text with no file |
| react-native-share | Needs a development build | App-specific targets, base64, Stories | Nothing much — it is the heavy option |
The practical shape of most apps: the built-in API for “share this recipe”, expo-sharing for “export my data”, and react-native-share only if a designer has specifically asked for an Instagram Stories button.
Text and links: the built-in API
import { Share, Platform } from 'react-native';
async function shareRecipe(recipe: { title: string; id: string }) {
const link = `https://yourapp.com/r/${recipe.id}`;
try {
const result = await Share.share(
{
// Android reads ONLY this field. Put the link in it.
message: `${recipe.title}\n\n${link}`,
// iOS uses this for a rich link preview; harmless elsewhere.
...(Platform.OS === 'ios' ? { url: link } : {}),
title: recipe.title, // Android chooser title / email subject
},
{ dialogTitle: 'Share recipe' },
);
if (result.action === Share.sharedAction) {
// result.activityType is iOS-only, and undefined for some targets.
track('recipe_shared', { via: result.activityType ?? 'unknown' });
}
} catch (e) {
// The user cancelling does NOT throw — this is a real failure.
console.warn('share failed', e);
}
}The asymmetries worth memorising:
urlis iOS-only. This single fact explains the majority of “sharing is broken on Android” reports. Duplicating the link intomessagecosts you a slightly noisier iOS payload and fixes Android completely.- Cancelling is not an error. Dismissing the sheet resolves with
dismissedActionon iOS rather than rejecting, so acatchthat shows an error toast will never fire for a cancel — and if yours does fire, something genuinely failed. titleis not a headline. It sets the Android chooser title and becomes the subject line when the target is email. It is invisible in most messaging apps.
Files: expo-sharing
The built-in API cannot attach a file. For an export, a receipt, or a generated image, the flow is always the same: produce bytes, write them to a local URI, hand that URI to the sheet.
npx expo install expo-sharing expo-file-system
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
async function exportCsv(rows: string[][]) {
if (!(await Sharing.isAvailableAsync())) {
// Sharing is unavailable on web and in some restricted contexts.
return Alert.alert('Sharing is not available on this device');
}
const csv = rows.map((r) => r.join(',')).join('\n');
// The filename is what the recipient sees. "export.csv" is a wasted opportunity.
const uri = FileSystem.cacheDirectory + `expenses-${new Date().toISOString().slice(0, 10)}.csv`;
await FileSystem.writeAsStringAsync(uri, csv, { encoding: FileSystem.EncodingType.UTF8 });
await Sharing.shareAsync(uri, {
mimeType: 'text/csv', // Android target filtering
UTI: 'public.comma-separated-values-text', // iOS target filtering
dialogTitle: 'Export expenses',
});
}Use cacheDirectory, not documentDirectory, for anything transient. The OS reclaims the cache under storage pressure, whereas the document directory is backed up and counted against the user’s storage forever — an export feature that quietly accumulates every CSV a user ever generated is a real bug that nobody notices for months. Set both mimeType and UTI: they are how each platform decides which apps appear in the sheet, and omitting them is why a PDF sometimes offers only “Save to Files”.
Sharing an image the app rendered
Share cards — a workout summary, a streak, a quote — are the highest-leverage share in most consumer apps, because an image travels further than a link. The recipe is a view capture plus the file flow above:
import { captureRef } from 'react-native-view-shot';
const cardRef = useRef<View>(null);
async function shareCard() {
const uri = await captureRef(cardRef, { format: 'png', quality: 1, result: 'tmpfile' });
await Sharing.shareAsync(uri, { mimeType: 'image/png', UTI: 'public.png' });
}
// Render the card at share dimensions, off-screen if it is not part of the layout:
<View ref={cardRef} collapsable={false} style={{ width: 1080, height: 1080, transform: [{ scale: 0.3 }] }}>
<ShareCard streak={streak} />
</View>collapsable={false} is required on Android: the platform optimises away views it thinks have no visual effect, and a captured view that got collapsed returns blank. And put the app name or handle inside the image itself. The caption is stripped the moment someone re-shares the screenshot, and the picture is the only part that survives.
The half most apps skip: the link going somewhere
A share is only worth building if the recipient ends up somewhere useful. Three things have to line up, and they are all outside the share call itself:
- The URL must be a real web page. A custom scheme like
yourapp://recipe/42renders as dead text for anyone who does not have the app. Share an https link and let universal links route it into the app when it is installed. - That page needs Open Graph tags. Messaging apps fetch the URL server-side to build the preview. No title, description, or image means your share arrives as a bare grey link, which is a measurable difference in whether anyone taps it.
- It should survive an install. A recipient without the app taps the link, sees the web page, installs, and lands on the home screen having lost the context entirely. Handling that properly means deferred deep linking, which is a real piece of work — worth doing only once sharing is measurably driving installs.
One more thing genuinely outside all three libraries: a share extension, the entry that makes your app appear in otherapps’ share sheets. That is native iOS and Android target configuration, not a JavaScript API, and no amount of react-native-share provides it.
Skip the boilerplate
Share sheet, file export, a card that renders at the right size — the same afternoon in every app. Describe yours at shipnative.dev and it generates a real React Native app with these patterns already in place, running on your phone in minutes, with the full Expo project available to export and own.