Native dynamic imports have been a long-awaited feature in the React Native community. Before the release of React Native version 0.72, dynamic imports had only been achievable through third-party libraries and other workarounds, such as using the React.lazy()
and Suspense
functions. Now, dynamic imports are a native part of the React Native framework.
In this article, we’ll compare static and dynamic imports, learn about how to work with dynamic imports natively, and best practices for effective implementation.
Static vs. dynamic imports
Before delving into the implementation details, it’s crucial to understand what dynamic imports are and how they differ from static imports, which are the more common way of including modules in JavaScript.
Static imports are the imports that you declare at the top of your file using the import
or require
syntax. This is because they will likely need to be available throughout your application when the application starts.
Here’s an example:
import React from 'react';import {View, Text} from 'react-native';const MyComponent = require('./MyComponent');
Static imports are synchronous, meaning they block the main thread until the module is completely loaded. This behavior can lead to slower app startup times, particularly in larger applications. Nevertheless, static imports shine when a library or module will be required multiple times or in multiple places in a codebase.
In contrast, dynamic imports empower developers to import modules on the fly, precisely when they’re needed, ushering in an asynchronous paradigm. This means that the code is loaded on demand.
In summary, the main difference between static and dynamic imports is that static imports are resolved at compile time, while dynamic imports are resolved at runtime.
Prior to React Native v0.72, dynamic imports were not supported out of the box because they were not compatible with the Metro bundler, which is responsible for bundling JavaScript code and assets in React Native applications.
The Metro bundler did not allow for any runtime changes and optimized the bundle size by removing unused modules and replacing them with static references. This meant that React Native developers had to rely on third-party libraries or custom solutions to achieve dynamic imports in their apps. We’ll explore these later in this article.
How to natively implement dynamic imports in React Native
To use native dynamic imports in React Native, you need to have React Native version 0.72 or higher installed. You can check your React Native version by running npx react-native --version
in your terminal. You also need to have Metro bundler version 0.66 or higher configured in your project.
There are two ways to use native dynamic imports in React Native: using the import()
syntax or using the require.context()
method.
Using the import()
syntax
According to Metro Bundler official documentation:
import()
calls are supported out of the box. In React Native, usingimport()
automatically splits your application code so that it loads faster during development, without affecting release builds.
The import()
syntax is similar to the static import
keyword but you can use it anywhere in your code, as long as you handle the promise resolution and rejection.
For example, suppose you have a component called SomeComponent
that you want to load dynamically based on some condition. You can use the import()
syntax like this:
const loadSomeComponent = async () => { try { const SomeComponent = await import('./SomeComponent'); // Do something with SomeComponent } catch (error) { // Handle error }};// Use SomeComponent conditionallyif (someCondition) { loadSomeComponent();}
Note: You need to use the await
keyword inside an async
function to wait for the promise to resolve. Alternatively, you can use the .then()
and .catch()
methods to handle the promise resolution and rejection.
Using the require.context()
method
The require.context()
method is now a supported feature of the Metro bundler that allows you to create a context for dynamic imports. This feature was added to the Metro library by Evan Bacon.
A context is an object that contains information about a set of modules or components that match a given pattern. You can use the require.context()
method to create a context like this:
// Create a context for all components in the ./components folderconst context = require.context('./components', true);
The first argument of the require.context()
method is the base directory where you want to look for modules or components. The second argument is a Boolean value that indicates whether you want to include subdirectories or not.
With the require.context
, you now can make imports based on a variable or a regex.
Here is an example of how you can use require.context
to import all the images from a folder and display them in a list:
// App.jsimport React from 'react';import {FlatList, Image, StyleSheet} from 'react-native';// Import all the images from the assets/images folderconst images = require.context('./assets/images', true, /\.png$/);// Create an array of image sourcesconst imageSources = images.keys().map((key) => images(key));const App = () => { // Render each image in a flat list return ( <FlatList data={imageSources} keyExtractor={(item) => item} renderItem={({item}) => <Image style={styles.image} source={item} />} /> );};const styles = StyleSheet.create({ image: { width: 100, height: 100, margin: 10, },});export default App;
React Native v0.72 introduced support for dynamic imports through the require.context
method, which is similar to the one provided by webpack.
But require.context
has long been available and used by Expo Router under the hood to automatically create routes based on the file directory structure and the files you have. It uses a require.context
call with a regex and the routes can all be figured out at runtime.
For example, if you have a file called app/home.tsx
, it will become a route with the path /home
. If you have a file called app/profile/settings.tsx
, it will become a route with the path /profile/settings
.
Therefore, you don’t need to manually define or import your routes — Expo Router will do it for you!
Third-party solutions to achieve dynamic imports
Using React.lazy()
and Suspense
React.lazy()
and Suspense
are features of React that allow you to render components lazily, meaning they are loaded only when they are rendered. You can use the React.lazy()
function to create a component that wraps a dynamic import, and you can use Suspense
to show a fallback component while the dynamic import is loading.
Here’s an example:
import React, { lazy, Suspense } from "react";import { Text, View } from "react-native";import { styles } from "./styles";const DynamicComponent = lazy(() => import("./DynamicComponent"));function App() { return ( <View style={styles.container}> <Suspense fallback={() => <Text>Loading ....</Text>}> <DynamicComponent /> </Suspense> </View> );}export default App;
Using React.lazy()
and Suspense
is a great way to implement dynamic imports in your React Native application. However, it’s important to note that React.lazy()
is specifically designed for code-splitting with React components. If you need to dynamically import non-component JavaScript modules, you might need to consider other approaches.
Loadable Components
Loadable Components is a way to split your React Native code into smaller chunks that can be loaded on demand. In React Native, you can use the react-loadable library to dynamically load and render components:
import Loadable from 'react-loadable';// Define a loading component while the target component is being loadedconst LoadingComponent = () => <ActivityIndicator size="large" color="#0000ff" />;// Create a dynamic loader for the target componentconst DynamicComponent = Loadable({ loader: () => import('./YourComponent'), // Specify the target component path loading: LoadingComponent, // Use the loading component while loading});// Use the dynamic component in your applicationfunction App() { return ( <View> <DynamicComponent /> </View> );}
In this code block:
- Import the
Loadable
function from thereact-loadable
library - Define a loading component (e.g., an
ActivityIndicator
) that will be shown while the target component is being loaded - Create a dynamic component by using the
Loadable
function. Provide theloader
property with a function that imports your target component (replace'./YourComponent'
with the actual path to your component) and specify theloading
property to display the loading component during the loading process - Finally, use the
DynamicComponent
within your app’s UI. It will dynamically load the target component and display it once it’s ready, showing the loading component in the meantime
This library was originally designed for React web applications, so it may not always work well in React Native.
Benefits of dynamic imports in React Native
Dynamic imports offer developers several advantages:
- Faster startup times: By only loading the required code on demand, dynamic imports can significantly reduce the time it takes for your app to start up. This is crucial for providing a smooth user experience, especially on slower devices or networks
- Improved code maintainability: Dynamic imports can help organize your codebase more effectively by allowing you to separate less frequently used components or libraries into separate modules. This can improve code maintainability and make it easier to work on specific parts of your app
- Progressive loading: Dynamic imports enable progressive loading. Instead of forcing users to wait for the entire application to load, you can prioritize the critical components and load secondary features in the background. This ensures a seamless initial experience for users, while the less essential parts of your app load in the background, keeping users engaged
- Optimized bundles: Dynamic imports allow you to optimize your JavaScript bundles by splitting them into smaller, more manageable chunks. This can lead to smaller bundle sizes, which in turn reduces the app’s memory footprint and accelerates the loading process
Best practices for using dynamic imports
To use dynamic imports effectively in React Native, here are some best practices that you should follow:
- Use dynamic imports sparingly: Dynamic imports are not a silver bullet that can solve all your performance and user experience issues. They come with some trade-offs, such as increased complexity, potential errors, and dependency on network connectivity. Therefore, you should use them only when necessary, and not overuse them
- Use loading indicators and placeholders: Loading indicators can show the user that the app is loading some modules dynamically and how long it will take. Placeholders can show the user what the app will look like when the modules are loaded and prevent layout shifts or blank spaces. You can use built-in components like
ActivityIndicator
orSkeleton
from React Native or third-party libraries likereact-native-loading-spinner-overlay
orreact-native-skeleton-placeholder
for this purpose - Use error boundaries and fallbacks: You should use error boundaries and fallbacks to handle errors and failures when using dynamic imports. Error boundaries are components that can catch and handle errors in their child components. Fallbacks are components that can be rendered instead of the original components when they fail to load or render. You can use built-in components like
ErrorBoundary
from React or third-party libraries like react-error-boundary orreact-native-error-boundary
for this purpose
Conclusion
In this article, we learned how to use native dynamic imports in React Native. With dynamic imports at your disposal, you have a powerful tool to make your React Native applications more efficient, responsive, and user-friendly. It’s crucial to use dynamic imports carefully and follow best practices to ensure a seamless user experience.
I hope you enjoyed this article, and learned something new and useful. If you have any questions or feedback, please feel free to leave a comment below. Thank you for reading! 😊
Happy coding!
LogRocket: Instantly recreate issues in your React Native apps
LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.