A React Native forum app's share-and-report sheet works on iOS, but on Android it crashes or silently does nothing; how do the iOS-only APIs fail there, and how do you guard them?
answer
- each iOS-only API fails differently
- ActionSheetIOS and DynamicColorIOS throw
- Alert.prompt returns silently
- Settings warns and returns null
- one wrapper per capability
basics
~20 sReact Native's iOS-only APIs fail on Android in three ways: ActionSheetIOS and DynamicColorIOS throw, Alert.prompt returns silently, and Settings warns and returns null. Guard each call site with Platform.OS or hide them behind one cross-platform wrapper per capability.
solid answer
~40 sThe iOS-only modules are exported on every platform, so imports never fail — only calls do, and each differently. `ActionSheetIOS.showActionSheetWithOptions` fails an invariant (`ActionSheetManager doesn't exist`) and throws. `DynamicColorIOS` throws on any non-iOS platform, even inside a `Platform.select` branch, because the object literal is evaluated first. `Alert.prompt` is wrapped in an iOS check and returns without a dialog, warning or callback. `Settings` resolves to a fallback that warns and returns `null`. So the sheet crashes when opened, the report-reason prompt silently never appears, and a preference reads as `null`. The fix is structural: one wrapper per capability — `showPostActions`, `askForText`, `readPreference` — that uses the iOS API on iOS and an app-rendered component or cross-platform library on Android, plus tests or device checks on both platforms for every flow that touches one.
code
tsx · 22 linesimport { ActionSheetIOS, Platform } from 'react-native';
export type PostAction = { title: string; run: () => void; destructive?: boolean };
// Feature code calls this; it never imports ActionSheetIOS directly.
export function showPostActions(actions: PostAction[], openAndroidSheet: (a: PostAction[]) => void) {
if (Platform.OS !== 'ios') {
openAndroidSheet(actions); // app-rendered bottom sheet
return;
}
const titles = [...actions.map(a => a.title), 'Cancel'];
ActionSheetIOS.showActionSheetWithOptions(
{
options: titles,
cancelButtonIndex: titles.length - 1,
destructiveButtonIndex: actions
.map((a, i) => (a.destructive ? i : -1))
.filter(i => i >= 0),
},
index => actions[index]?.run(),
);
}go deeper
Recall that ActionSheetIOS, DynamicColorIOS, Alert.prompt and Settings are iOS-only and must be guarded before calling them on Android.
Explain how each one fails on Android — two throw, one is silent, one warns — and why Platform.select does not stop the iOS call.
Diagnose Android crash and no-op reports back to iOS-only calls and replace scattered guards with capability wrappers, lint rules and device checks.
Set the platform-divergence policy: which native iOS experiences are worth a separate Android implementation and who maintains both.
## The scenario A community forum app lets users long-press a post to **share** or **report** it. The iOS build uses `ActionSheetIOS` for the menu, `Alert.prompt` for the report reason, `DynamicColorIOS` for the sheet's accent colour and `Settings` for a "confirm before reporting" preference. Android users file two kinds of reports: *the app closes when I long-press a post*, and *reporting does nothing*. ## How each iOS-only API behaves on Android React Native exports these modules from `react-native` on every platform, so `import { ActionSheetIOS } from 'react-native'` compiles and loads on Android. The behaviour only diverges when the code **calls** them: | API | On Android | Why | | --- | --- | --- | | `ActionSheetIOS.showActionSheetWithOptions` | **Throws** `ActionSheetManager doesn't exist` | The native module is only registered on iOS; the wrapper's `invariant` fails | | `DynamicColorIOS({ light, dark })` | **Throws** `DynamicColorIOS is not available on this platform.` | The non-iOS implementation is a single `throw` | | `Alert.prompt(...)` | **Returns silently** | The body is inside `if (Platform.OS === 'ios')` with no else | | `Settings.get/set/watchKeys` | **Warns**, `get` returns `null` | A fallback module replaces it off iOS | Mapping the bug reports: 1. *"The app closes on long-press"* — `ActionSheetIOS` threw in the press handler; in a release build an uncaught error like that can take the app down. 2. *"Reporting does nothing"* — `Alert.prompt` returned without showing anything, so `submitReport` was never called. 3. A less visible one: the preference read as `null`, so the confirm step was always skipped. 4. If `DynamicColorIOS` had been called at the top of a style module, Android would have failed as soon as that module was imported, not on the long-press. ## The eager-evaluation trap A common "guard" does not guard: ```tsx const accent = Platform.select({ ios: DynamicColorIOS({ light: '#0a84ff', dark: '#64d2ff' }), android: '#1a73e8', }); ``` `Platform.select` receives an already-built object; the `ios` value is computed on Android too, and it throws. Guards must wrap the **call**, not choose between precomputed results. ## Triage steps 1. **Reproduce on an Android device** with the exact gesture: long-press a post, pick *Report*. 2. **Read the error** in the development build: `ActionSheetManager doesn't exist` or `DynamicColorIOS is not available on this platform.` names the culprit directly. 3. **For "nothing happens"**, look for calls with no Android branch — `Alert.prompt` first, then any `Settings.get` whose `null` result changes behaviour. 4. **Search shared code** for `ActionSheetIOS`, `DynamicColorIOS`, `Alert.prompt` and `Settings.` to find every other call site before fixing only the reported one. ## Guarding them structurally Per-call-site `if (Platform.OS === 'ios')` checks work, but they spread and get forgotten. A sturdier shape: - **One wrapper per capability**, named for what the feature needs rather than for the API: `showPostActions(actions)`, `askForText(options)`, `readPreference(key)`, `accentColor`. - **iOS branch** uses the native API; **Android branch** renders an app component (a bottom sheet, a `Modal` with a `TextInput`) or uses a cross-platform storage library. - **Feature code imports only the wrappers.** A lint rule that forbids importing `ActionSheetIOS`, `DynamicColorIOS` and `Settings` outside the wrapper folder, and forbids `Alert.prompt` elsewhere, keeps it that way. - **The split itself** can be an inline `Platform.OS` branch or per-platform files; which to choose is a separate decision about how platform code is organised. ## Verifying - Add a **unit test per wrapper** that runs with the platform mocked to Android and asserts the Android path is taken. - Put **every flow that touches an iOS-only API** on the Android device checklist; silent no-ops are only caught by someone looking at the screen. - Search the codebase for the iOS-only names during review, especially in shared components. ## Interview summary A strong answer lists the failure modes per API — throw, throw, silent, warn-and-null — explains the `Platform.select` trap, and proposes capability wrappers with an Android implementation, enforced by lint and checked on devices.
- Why doesn't importing ActionSheetIOS from 'react-native' fail on Android?React Native exports it on every platform through a lazy getter, and the JavaScript wrapper loads fine. Only the native module lookup returns nothing on Android, so the failure appears when `showActionSheetWithOptions` runs and its invariant check throws.
- Which of React Native's iOS-only APIs is hardest to catch in testing on Android, and why?`Alert.prompt`, because it returns silently: no exception, no warning, no callback. A crash or a warning shows up in development, but a missing dialog is only noticed by someone who expects it and looks at the screen.
saying these in an interview costs you the question
- Importing an iOS-only API crashes Android at startup
- Platform.select keeps iOS-only calls from running on Android
- All iOS-only APIs throw on Android
- Alert.prompt logs a warning on Android
- Checking Platform.OS once in the root component protects every screen