Implement dynamic import natively in React Native

In the React Native community, native dynamic import has been a long-awaited feature. Before the release of React Native 0.72, dynamic import could only be achieved through third-party libraries and other workarounds, such as using the React.lazy()and Suspensefunctions. Dynamic imports are now a native part of the React Native framework.

In this article, we will compare static and dynamic imports, learn how to handle dynamic imports natively, and best practices for effective implementation.

Static import vs. dynamic import

Before delving into the implementation details, it’s crucial to understand what dynamic imports are and how they differ from static imports, the more common way of including modules in JavaScript.

Static imports are imports that you declare at the top of your file using the importor syntax. requireThis is because they may need to be available throughout your application when the application starts.

Here is 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 fully loaded. This behavior can cause slower application startup times, especially in larger applications. However, static imports can be very useful when a library or module is needed at multiple times or in multiple places in the code base.

In contrast, dynamic import gives developers the ability to import modules instantly when needed, ushering in an asynchronous paradigm. This means the code is loaded on demand.

Overall, the main difference between static imports 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 packaging JavaScript code and assets in React Native applications.

The Metro packager does not allow any runtime changes and optimizes package size by removing unused modules and replacing them with static references. This means that React Native developers must rely on third-party libraries or custom solutions to implement dynamic imports in their apps. We will explore these later in this article.

How to implement dynamic import natively in React Native

To use native dynamic imports in React Native, you need to install React Native version 0.72 or higher. You can npx react-native --versioncheck your React Native version by running in the terminal. You will also need to configure version 0.66 or higher of the Metro packager in your project.

There are two ways to use native dynamic import in React Native: using import()syntax or using require.context()method.

Use import() syntax

According to Metro Bundler official documentation:

import()The call is supported out of the box. In React Native, using 

import()will automatically split your application code, making it load faster during development without affecting release builds.

import()The syntax is similar to the static importkeyword, but you can use it anywhere in your code as long as you take care of promise resolution and rejection.

For example, let’s say you have a SomeComponentcomponent called and you want to dynamically load it based on certain conditions. You can use syntax like this import():

const loadSomeComponent = async () => {
   try {
     const SomeComponent = await  import ( './SomeComponent' );
     // Do something with SomeComponent 
  } catch (error) {
     // Handle error
  }
};

// Use SomeComponent conditionally 
if (someCondition) {
  loadSomeComponent();
}

Note: You need to asyncuse awaitthe keyword inside the function to wait for the promise to resolve. Alternatively, you can use the .then()and .catch()methods to handle promise resolution and rejection.

Use require.context() method

require.context()Methods are now a supported feature of the Metro packager, allowing you to create a context for dynamic imports. This feature was added to the Metro library by Evan Bacon.

contextIs an object containing information about a set of modules or components that match a given pattern. You can use require.context()the method to create such a context:

// Create a context for all components in the ./components folder 
const context = require . context ( './components' , true );

require.context()The first argument to the method is the base directory in which you want to look for modules or components. The second parameter is a Boolean value indicating whether you want to include subdirectories.

With that require.context, you can now import based on variables or regular expressions.

Here’s an example showing how to require.contextimport all pictures from a folder and display them in a list:

// App.js 
import  React  from  'react' ;
 import { FlatList , Image , StyleSheet } from  'react-native' ;

// Import all the images from the assets/images folder 
const images = require . context ( './assets/images' , true , /\.png$/ );

// Create an array of image sources 
const 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 require.contexsupport for dynamic imports via the t method, similar to what webpack offers.

But require.contexthas always been used by Expo Router behind the scenes to automatically create routes based on the file directory structure and the files you have. It uses a call with a regular expression require.contextand all routes can be determined at runtime.

For example, if you have a app/home.tsxfile named , 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.

For example, if you have a app/home.tsxfile named , it will become a /homeroute with path . If you have a app/profile/settings.tsxfile named , it will become a /profile/settingsroute with path .

Therefore, you don’t need to manually define or import your routes – Expo Router does it for you!

Third-party solution for dynamic import

Using React.lazy() and Suspense

React.lazy()and Suspenseare features of React that allow you to lazy load components, that is, only load them when they are rendered. You can use React.lazy()the function to create a component that wraps a dynamic import, and you can use Suspense to display a standby component while the dynamic import is loading.

Here is 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 ;

React.lazy()Using and Suspenseis 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 of React components. If you need to dynamically import non-component JavaScript modules, you may want to consider other methods.

Loadable components

Loadable Components are a way to split your React Native code into smaller chunks that can be loaded on demand. In React Native, you can use react-loadablelibraries to dynamically load and render components.

import  Loadable  from  'react-loadable' ;

// Define a loading component while the target component is being loaded 
const  LoadingComponent = () => < ActivityIndicator  size = "large"  color = "#0000ff" /> ;

// Create a dynamic loader for the target component 
const  DynamicComponent = Loadable ({
   loader : () =>  import ( './YourComponent' ), // Specify the target component path 
  loading : LoadingComponent , // Use the loading component while loading
});

// Use the dynamic component in your application 
function  App () {
   return (
     < View > 
      < DynamicComponent /> 
    </ View >
  );
}

In this code:

  • Import function from react-loadablelibraryLoadable
  • Define a loading component (for example, an ActivityIndicator) that will be displayed when the target component is loaded.
  • Create a dynamic component using the Loadable function. Provide the loader attribute with a function that imports the target component (replace './YourComponent'with the actual path to the component), and specify loadingthe attribute to display the loaded component during the loading process.
  • Finally, use it in your app’s user interface DynamicComponent. It will dynamically load the target component and display it when ready while showing the loading component.

This library was originally designed for React web apps, so it may not always work well in React Native.

Benefits of dynamic imports in React Native

Dynamic imports provide developers with several advantages:

  • Faster startup time : Dynamic imports can significantly reduce the time it takes for your app to start by loading only the code you need. This is critical to providing a smooth user experience, especially if the device or network is slow.
  • Improve code maintainability : Dynamic imports can organize your code base more efficiently by letting you separate components or libraries that are not commonly used into separate modules. This can improve the maintainability of your code, making it easier to work on specific parts of your application.
  • Progressive loading: Dynamic import supports progressive loading. Instead of forcing the user to wait for the entire app to load, you can load critical components first, while secondary functionality loads in the background. This ensures that the user’s initial experience is seamless while less important 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 a reduction in bundle size, thereby reducing the application’s memory footprint and speeding up the loading process.

Best practices for using dynamic imports

  • Use dynamic imports with caution: Dynamic imports are not a panacea for all your performance and user experience problems. They come with trade-offs such as increased complexity, potential for errors, and dependence on network connectivity. Therefore, you should only use them when necessary and not overuse them.
  • Use loading indicators and placeholders: Loading indicators can show users that the app is dynamically loading some modules and how long it takes. Placeholders can show the user what the application will look like when the module is loaded and prevent layout changes or empty space. You can use ActivityIndicatorReact Native built-in components like Skeleton or a third-party library like react-native-loading-spinner-overlay or react-native-skeleton-placeholderto achieve this.
  • Use error boundaries and fallbacks: When using dynamic imports, you should use error boundaries and fallbacks to handle errors and failures. An error boundary is a component that can catch and handle errors in its child components. Fallbacks are components that can be rendered if the original component fails to load or render. You can use ErrorBoundarybuilt-in components like React or a third-party library like react-error-boundaryor react-native-error-boundaryto achieve this.

Summarize

In this article, we learned how to use native dynamic imports in React Native. With the powerful tool of dynamic import, you can make your React Native application more efficient, responsive, and user-friendly. It’s crucial to use dynamic imports with caution and follow best practices to ensure a seamless user experience.