Optional Imports in React Native
Optional imports are useful when a React Native app has platform-specific code, feature-gated screens, optional assets, or native modules that should only load in one environment. They are also easy to misuse. Metro still needs to know what files can be bundled, so arbitrary runtime strings are not a good import strategy.

Quick Answer
Use explicit patterns that Metro and TypeScript can understand:
- platform-specific files for iOS, Android, native, or web differences;
Platform.selectfor small platform branches;- static module maps for optional assets or screens;
import()for known modules that can be lazy-loaded;import typewhen you only need TypeScript types.
Avoid production code that tries to load random dependency names with
try/catch require(...). If a dependency is required for a feature, declare it
in package.json, install it through the project's package manager, and rebuild
native apps when native code is involved.
For current local setup, use Set Up Your React Native Development Environment and Current React Native Stack. For bundler issues, use Metro and Bundler Errors.
Prefer Platform-Specific Files for Real Platform Differences
When the implementation is meaningfully different by platform, create separate files and import the module without the platform suffix:
src/services/share.native.ts
src/services/share.web.ts
src/components/DatePicker.ios.tsx
src/components/DatePicker.android.tsx
Then import normally:
import { shareListing } from './services/share';
import DatePicker from './components/DatePicker';
Metro resolves the right file for the current target. This keeps your component clean and prevents unsupported native code from being loaded on the wrong platform.
Use Platform.select for Small Branches
For small differences, keep the branch close to the code:
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
screen: {
flex: 1,
...Platform.select({
ios: {
paddingTop: 12,
},
android: {
paddingTop: 8,
},
default: {
paddingTop: 10,
},
}),
},
});
You can also use Platform.select to choose a component loader, but keep the
set of possible modules explicit:
import { Platform } from 'react-native';
const loadNativePicker = Platform.select({
ios: () => import('./pickers/DatePickerIOS'),
android: () => import('./pickers/DatePickerAndroid'),
default: () => import('./pickers/DatePickerFallback'),
});
export async function getDatePicker() {
return loadNativePicker?.();
}
Mega Bundle Sale is ON! Get ALL of our React Native codebases at 90% OFF discount 🔥
Get the Mega BundleUse Static Module Maps for Optional Screens
If a feature flag decides which screen is available, create a static map instead of building an import path from user input or remote config:
const optionalScreens = {
analytics: () => import('./screens/AnalyticsScreen'),
subscriptions: () => import('./screens/SubscriptionsScreen'),
onboarding: () => import('./screens/OnboardingScreen'),
} as const;
type OptionalScreenKey = keyof typeof optionalScreens;
export async function loadOptionalScreen(key: OptionalScreenKey) {
return optionalScreens[key]();
}
This gives Metro a finite list of files, keeps TypeScript strict, and avoids loading a module that was never meant to be part of the app.
Use Static require for Images and Local Assets
React Native image assets should usually be statically required. If the image is optional, map the allowed choices:
const profileImages = {
default: require('./assets/profile-default.png'),
premium: require('./assets/profile-premium.png'),
team: require('./assets/profile-team.png'),
} as const;
type ProfileImageKey = keyof typeof profileImages;
export function getProfileImage(key: ProfileImageKey) {
return profileImages[key] ?? profileImages.default;
}
Do not use a dynamic path like require('./assets/' + name + '.png'). Metro
cannot reliably include arbitrary files it cannot see during bundling.
Use import() for Known Lazy Modules
Metro supports import() for known modules. In React Native, this can improve
development loading behavior, but it should not be treated as a guaranteed
release bundle size optimization by itself.
import React, { Suspense } from 'react';
import { ActivityIndicator } from 'react-native';
const HeavySettingsScreen = React.lazy(() => import('./HeavySettingsScreen'));
export function SettingsRoute() {
return (
<Suspense fallback={<ActivityIndicator />}>
<HeavySettingsScreen />
</Suspense>
);
}
Use this for known screens and expensive modules. Do not use it to hide missing dependencies or to import names coming from untrusted input.
Use Type-Only Imports in TypeScript
If a module is only needed for types, make that explicit:
import type { FirebaseFirestoreTypes } from '@react-native-firebase/firestore';
export type UserDocument =
FirebaseFirestoreTypes.QueryDocumentSnapshot<UserProfile>;
Type-only imports help TypeScript express intent and avoid accidental runtime imports for code that should only exist during type checking.
Be Careful With Backward Compatibility Shims
Old React Native tutorials often showed this pattern:
import { AsyncStorage } from 'react-native';
let storage = AsyncStorage;
if (!storage) {
storage = require('@react-native-async-storage/async-storage').default;
}
That pattern is no longer a good default. AsyncStorage is not part of modern
React Native core. Install and import the maintained package directly:
import AsyncStorage from '@react-native-async-storage/async-storage';
If you support an older app package, keep the compatibility code isolated in one adapter module and document which app versions still need it.
Looking for a custom mobile application?
Our team of expert mobile developers can help you build a custom mobile app that meets your specific needs.
Get in TouchFAQ
Can React Native import files dynamically?
React Native supports import() for known modules through Metro. The import
target still needs to be something the bundler can discover. Arbitrary runtime
paths are not a reliable app architecture.
Should I use try/catch require for optional packages?
Not for normal app code. Declare dependencies in package.json and make feature
availability explicit with feature flags, platform files, or adapter modules.
How should I handle web-only or native-only code?
Use .web.tsx, .native.tsx, .ios.tsx, and .android.tsx files when the
implementation differs by target. It is clearer than runtime branching for large
components.