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 Suspense
functions. 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.
Contents
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 import
or syntax. require
This 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 --version
check 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 import
keyword, 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 SomeComponent
component 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 async
use await
the 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.
context
Is 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.context
import 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.contex
support for dynamic imports via the t method, similar to what webpack offers.
But require.context
has 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.context
and all routes can be determined at runtime.
For example, if you have a app/home.tsx
file 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.tsx
file named , it will become a /home
route with path . If you have a app/profile/settings.tsx
file named , it will become a /profile/settings
route 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 Suspense
are 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 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 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-loadable
libraries 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-loadable
libraryLoadable
- 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 specifyloading
the 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
ActivityIndicator
React Native built-in components like Skeleton or a third-party library likereact-native-loading-spinner-overlay
orreact-native-skeleton-placeholder
to 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
ErrorBoundary
built-in components like React or a third-party library likereact-error-boundary
orreact-native-error-boundary
to 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.