Skip to content

Media, Sharing, and Audio Recipes

A React Native guide for coding agents. Also covers incoming share code examples, share extension, share images, expo sharing, expo audio, media lifecycle.

Incoming share URIs and generated media are lifecycle-sensitive. Copy content you need to keep, clear consumed payloads, give processing a visible state, and release long-lived native resources.

Route Incoming Shares to One Handler

Keep native intent parsing defensive. path may be an arbitrary provider string, not a valid URL.

// app/+native-intent.ts
export function redirectSystemPath({ path }: { path: string; initial: boolean }): string {
  try {
    const url = new URL(path, "myapp://home");
    if (url.hostname === "expo-sharing") return "/share/import";
    return url.pathname.startsWith("/") ? `${url.pathname}${url.search}` : "/home";
  } catch {
    return "/share/error";
  }
}

Gate the redirect on the incoming share-intent host instead of cached payload state, which can remain populated after the original launch. Never throw from redirectSystemPath; route to a recoverable error screen. Native-intent rewriting has no application context, so perform authentication and ownership checks after navigation.

For native app configuration, durable intake, Expo SDK compatibility, and multi-destination delivery, load share-extension-recipes.

Copy an Incoming Image Before Clearing It

Provider-backed content URIs may be temporary. Copy the selected file into app storage, then clear the share payload so it is not processed again.

import { useState } from "react";
import { File, Paths } from "expo-file-system";
import { useIncomingShare } from "expo-sharing";
import { Button, Text, View } from "react-native";

type ImportState =
  | { status: "idle" }
  | { status: "saving" }
  | { status: "saved"; uri: string }
  | { status: "error"; message: string };

export function ShareImportScreen() {
  const { resolvedSharedPayloads, isResolving, error, clearSharedPayloads } =
    useIncomingShare();
  const [state, setState] = useState<ImportState>({ status: "idle" });

  const image = resolvedSharedPayloads.find(
    (payload) => payload.contentType === "image" && payload.contentUri,
  );

  async function saveImage() {
    if (!image?.contentUri) return;
    setState({ status: "saving" });

    try {
      const source = new File(image.contentUri);
      const extension = source.extension || ".jpg";
      const destination = new File(Paths.document, `import-${Date.now()}${extension}`);
      await source.copy(destination);
      clearSharedPayloads();
      setState({ status: "saved", uri: destination.uri });
    } catch {
      setState({ status: "error", message: "The image could not be saved." });
    }
  }

  if (isResolving) return <Text accessibilityRole="progressbar">Reading shared image…</Text>;
  if (error) return <Text accessibilityRole="alert">The shared item could not be read.</Text>;

  return (
    <View style={{ gap: 12, padding: 16 }}>
      <Text>{image ? "Image ready to import" : "Share an image to continue"}</Text>
      <Button title={state.status === "saving" ? "Saving…" : "Save image"} onPress={saveImage} disabled={!image || state.status === "saving"} />
      {state.status === "error" ? <Text accessibilityRole="alert">{state.message}</Text> : null}
    </View>
  );
}

For several payloads, assign each a stable import id and persist completion before clearing. This makes re-entry idempotent if the app is killed mid-import.

Share Only a Local File

shareAsync expects a local file URL. Download or render remote output into cache first.

import { File, Paths } from "expo-file-system";
import * as Sharing from "expo-sharing";

export async function shareGeneratedImage(remoteUrl: string) {
  const destination = new File(Paths.cache, `share-${Date.now()}.png`);
  const localFile = await File.downloadFileAsync(remoteUrl, destination);

  if (!(await Sharing.isAvailableAsync())) {
    throw new Error("Sharing is unavailable on this device");
  }

  await Sharing.shareAsync(localFile.uri, {
    mimeType: "image/png",
    dialogTitle: "Share image",
  });
}

Cache is appropriate for disposable exports. Store user-created originals in the document directory or your durable backend.

Let Audio Hooks Own Playback

Use useAudioPlayer for screen-scoped playback because the hook releases the player on unmount.

import { useAudioPlayer, useAudioPlayerStatus } from "expo-audio";
import { Button, Text, View } from "react-native";

export function AudioPreview({ uri }: { uri: string }) {
  const player = useAudioPlayer({ uri });
  const status = useAudioPlayerStatus(player);

  return (
    <View style={{ gap: 8 }}>
      <Text>{status.isLoaded ? "Preview ready" : "Loading preview…"}</Text>
      <Button
        title={status.playing ? "Pause" : "Play"}
        disabled={!status.isLoaded}
        onPress={() => (status.playing ? player.pause() : player.play())}
      />
    </View>
  );
}

If you create a player outside a hook so it can outlive a screen, call release() when its owner is destroyed. Stop or pause audio when the user leaves a voice flow unless background playback is intentional and configured.

Media UX and Safety Checks

  • Explain why camera, photo, or microphone permission is needed before the system prompt.
  • Offer a file picker or text path when media access is optional.
  • Validate MIME type, file size, and dimensions on the server.
  • Show separate reading, uploading, processing, and failed states.
  • Do not claim an upload is complete until the server has accepted it.
  • Clear temporary files on success and bounded cleanup schedules.

Sources

Behavior is checked against Expo’s current Sharing, FileSystem, native intent, and Audio documentation. The import state machine and lifecycle policy are independently written.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-react-native-guide({ topic: "media-sharing-audio-recipes" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems