Implementing Code Splitting in React Native with Re.Pack

Authors
Paweł Trysła
Software Engineer
@
Callstack

In the previous article about code splitting in React Native, we’ve talked about what code splitting and bundling can be used for. It was a high-level introduction to the topic.  In this article, which is a technical one, we focus on implementation and approaches including the one with Re.Pack - an Open Source, Webpack-based toolkit to build your React Native application with the full support of the Webpack ecosystem.

Re.Pack - Webpack-based toolkit to build your React Native application

As we mentioned before, Re.Pack is an Open Source toolkit that allows developers to use Webpack features in building React Native mobile apps (especially in building super apps and mini apps).

One of the most important superpowers of Re.Pack is the fact that the toolkit allows developers to implement dynamic content/feature delivery in native and cross-platform apps. Thanks to that, Re.Pack can be a great ally in optimizing the performance stats of the React Native apps, especially when it comes to startup time or the initial download size of the apps.

The toolkit is the successor of Haul, but has a wide range of new features that were not available in Haul including:

  • Full Webpack ecosystem of plugins and loaders
  • Built-in Flipper support
  • Hermes support
  • Built-in Hot Module Replacement + React Refresh support
  • Asynchronous chunks support
  • Fully-featured development server

and many more! The full list of Re.Pack features and documentary can be found on its Github profile.

Learn more: If you haven't heard anything about Re.Pack yet, check out the 7th episode The React Native Show podcast titled "Re.Pack - bringing Webpack to React Native". In this talk Paweł Trysła (aka. Zamotany), the lead engineer at Re.Pack, explains what Re.Pack is, what are the benefits of using this toolkit and discusses the use cases of the toolkit. A must watch!

As part of our podcast, we released a podcast dedicated to React native news, where you can find more about it.

Implementation of dynamic content in mobile apps - available solutions

Having explained what Re.Pack and code splitting are, let’s explore available options for implementing dynamic content/feature delivery in native and cross-platform mobile applications.

Dynamic content in native applications

Although Android and iOS are not the only native platforms in the world, we will focus on them as they are the most popular native operating systems now.

Android

For Android, one of the ways to implement any dynamic content or feature is to use WebView. While this is the easiest method, it’s also the one with subpar experience - higher resources usage and no native look and feel.
Luckily, there’s a better solution - Play Feature Delivery. These options allow us to create bundles with features using a native language (Java or Kotlin).

iOS

On iOS, WebView is our only option. Assuming that we could inject binary code into the running application, the solution would be breaking Apple’s iOS application guidelines, meaning it’s unlikely our application would pass Apple’s App Store review.

Dynamic content in cross-platform apps

Developing a native mobile application with dynamic feature/content delivery means we not only have to write the code twice using different languages but also using vastly different architectures and subpar experience on iOS.

What about dynamic content in cross-platform development? Let’s see how we can implement it with React Native.

React Native with Metro

Given that React Native uses JavaScript as a language for running out application’s logic, we could instruct React Native to execute new pieces of JavaScript code on demand. However, there’s no such publicly available API in React Native, so you would need to implement a Native Module in order to be able to execute arbitrary JavaScript code.

The next step would be to figure out how to actually split the code using Metro. You might be able to use Metro’s segment API but it’s not widely used and the documentation is almost non-existent.

Keep in mind that if you plan to allow 3rd party developers to provide parts of your application functionality (e.g. 3rd party mini-apps), then you would need to figure out a custom solution to do so and find a way to share at least React and React Native dependencies. Metro isn’t a good candidate for that, as the output is a self-contained executable bundle, while you would need a library-like bundle for sharing purposes.

React Native with Webpack and Re.Pack

With Webpack you are able to introduce code splitting, regardless of whether you want to allow 3rd party code or not. Webpack and its features give developers unique possibilities, including:

  • dynamic import() function which allows to create asynchronous chunks
  • ability to share dependencies between individual builds
  • ability to create library-like bundles and much more

With Re.Pack, you are able to leverage the aforementioned Webpack features inside React Native applications including a Native Module to load additional JavaScript code called <rte-code>ChunkManager<rte-code>.

To sum up, the combination of Webpack and Re.Pack allows us to deliver dynamic content delivery with native-like experience across multiple platforms with minimal effort.

Code Splitting in React Native with Webpack and Re.Pack

Let’s see how code splitting in React Native looks like and works under the hood with Webpack and Re.Pack. But, before we dive deep, let’s establish what a chunk is and what its types are.

Chunk

In Re.Pack a chunk is an additional JavaScript (or Hermes’ bytecode) file. That’s it, we don’t differentiate between how the chunk was created - it will be important later when we’ll talk about approaches to code splitting.

There are 2 types of chunks in the context of Re.Pack and React Native:

  • local chunk - an additional JS file packed together with the main bundle and shipped to the end user inside an ipa/apk file;
  • remote chunk - an additional JS file hosted on the remote server and downloaded by the application on the end user’s device on demand.

ChunkManager

The central point of code splitting support with Re.Pack is the ChunkManager API. It’s provided by the Re.Pack as a native module and allows you to execute additional JavaScript code.
Before ChunkManger is ready to be used, it needs to be configured using <rte-code>ChunkManager.configure(…)<rte-code> function:

import { ChunkManager } from '@callstack/repack/client';
import AsyncStorage from '@react-native-async-storage/async-storage';
ChunkManager.configure({
// Optional: Will cache resolved URLs from `resolveRemoteChunk`
// and prevent chunks overfetching - if the URL from cache and
// URL from `resolveRemoteChunk` are the same, then it will reuse/load
// previosuly downloaded chunk.
storage: AsyncStorage,
// Resolve URL from where to download a chunk.
resolveRemoteChunk: async (chunkId) => {
// You can fetch remote config or obtain a base URL dynamically.
return {
// Retrun a URL to a chunk
url: `https://domain.exaple/apps/${chunkId}`,
};
},
});
view raw gistfile1.txt hosted with ❤ by GitHub

This code instructs <rte-code>ChunkManager<rte-code> how to resolve remote chunks – what the URL of the chunk is that will be downloaded and executed. There’s no need to specify how to resolve local chunks.

The ChunkManager exposes a function called <rte-code>loadChunk(chunkId: string)<rte-code>. It uses <rte-code>resolveRemoteChunk<rte-code> function from <rte-code>ChunkManager.configure(…)<rte-code> to obtain a URL and passes it to a native site to download the chunk and execute it.
Whether you should call this function yourself or not depends on which approach to code splitting you choose - more on that later.

The <rte-code>ChunkManager<rte-code> should be used as a foundation for implementing code splitting and dynamic content delivery.

Approaches to implement Code splitting in React Native

Now, let’s talk about the aforementioned approaches to code splitting and, in general, dynamic content delivery:

Async chunks (dynamic import(…) function)

The easiest approach is to use the dynamic <rte-code>import()<rte-code> function. It produces additional chunks as part of the same compilation (build process).

diagram of dynamic import() function

‍

The file passed to the <rte-code>import()<rte-code> function will become an entry point to the new chunk.

import { ChunkManager } from '@callstack/repack/client';
ChunkManager.configure({
resolveRemoteChunk: async (chunkId) => {
return {
url: `http://domain.exaple/apps/${chunkId}`,
};
},
});
const teacherModule = await import('./teacher.js')
view raw gistfile1.txt hosted with ❤ by GitHub

It allows you to quickly introduce code splitting, because most of the heavy lifting is handled by Webpack’s runtime code.

You can use <rte-code>import()<rte-code> function together with <rte-code>React.lazy()<rte-code>:

import * as React from 'react';
import { ChunkManager } from '@callstack/repack/client';
ChunkManager.configure({
resolveRemoteChunk: async (chunkId) => {
return {
url: `http://domain.exaple/apps/${chunkId}`,
};
},
});
const TeacherModule = React.lazy(() => import('./Teacher.js'));
const StudentModule = React.lazy(() => import('./Student.js'));
export function App({ role }) {
if (role === 'teacher') {
return <TeacherModule />;
}
return <StudentModule />
}
view raw gistfile1.txt hosted with ❤ by GitHub

or to build in-house Mini-apps:

import * as React from 'react';
import { ScrollView, View, Text, Button } from 'react-native';
import { ChunkManager } from '@callstack/repack/client';
ChunkManager.configure({
resolveRemoteChunk: async (chunkId) => {
return {
url: `http://domain.exaple/apps/${chunkId}`,
};
},
});
const apps = [
{
id: 'english-module', // Notice: `id` is the same as `webpackChunkName` magic comment
title: 'Learn English',
load: () =>
import(/* webpackChunkName: "english-module" */ './english-module.js'),
},
{
id: 'spanish-module',
title: 'Learn Snapish',
load: () =>
import(/* webpackChunkName: "spanish-module" */ './spanish-module.js'),
},
];
const AppsContext = React.createContext();
function AppsProvider({ children }) {
const [installedApps, setInstalledApps] = React.useState([]);
const [exports, setExports] = React.useState({});
const installApp = React.useCallback(async (appId) => {
const app = apps.find((app) => app.id === appId);
if (app) {
const appExports = await app.load();
setInstalledApps((installedApps) => [...installedApps, appId]);
setExports((exports) => ({ ...exports, [appId]: appExports }));
}
}, []);
const uninstallApp = React.useCallback((appId) => {
setInstalledApps((installedApps) =>
installedApps.filter((installedApp) => installedApp.id !== appId)
);
setExports((exports) => ({ ...exports, [appId]: undefined }));
ChunkManager.invalidateChunks(appId);
}, []);
return (
<AppContext.Provider
value={{
installApp,
uninstallApp,
installedApps,
exports,
}}
>
{children}
</AppContext.Provider>
);
}
// Somewhere
function LanguageStore() {
const { installedApps, installApp, uninstallApp } =
React.useContext(AppsContext);
return (
<ScrollView>
{apps.map((app) => (
<View>
<Text>{app.title}</Text>
{installedApps.includes(app.id) ? (
<Button
onPress={() => uninstallApp(app.id)}
title="Remove module"
/>
) : (
<Button onPress={() => installApp(app.id)} title="Add module" />
)}
</View>
))}
</ScrollView>
);
}
// And then
function AppRenderer({ appId }) {
const { exports } = React.useContext(AppsContext);
// You can customize what should happen if exports for the app are not found.
const Component = exports[appId]?.RootComponent ?? (() => null);
return <Component />;
}
view raw gistfile1.txt hosted with ❤ by GitHub

By default, all chunks created by the dynamic <rte-code>import()<rte-code> function are remote chunks. To mark a chunk as local, you provide a name (or RegExp) of a chunk in <rte-code>OutputPlugin<rte-code> in Webpack config:

// ...
module.exports = {
// ...
plugins: [
// ...
new ReactNative.OutputPlugin({
// ...
// Specify which chunk is local chunk
localChunks: [/Student/],
}),
// ...
],
};
view raw gistfile1.txt hosted with ❤ by GitHub

The most noticeable limitation of this approach is the fact that the chunks are created as part of a single build process. There’s no option to include unknown 3rd party code dynamically - the code has to be available at build time.

Dynamic scripts in Re.Pack

Re.Pack’s <rte-code>ChunkManager<rte-code> allows you to load and execute arbitrary code, so it’s possible to use dynamic script-like chunks - chunks with code that is unknown at build time.

Implementing Code Splitting in React Native with Re.Pack

‍

This allows the creation of modular applications, mini-apps/super apps with 3rd-party provided code. There are multiple ways for third-party developers to create such chunks. One which we recommend is to leverage Webpack’s library feature together with externals.

Code Splitting in React Native with Re.Pack

Module Federation

This approach is the most complex one since it relies on a brand new feature of Webpack 5 - Module Federation. You can read more about it here, but in short, it allows us to create mini-apps/super-apps using independent and isolated codebases:

module federation Re.Pack

‍

This approach is recommended for the most advanced and enterprise users, due to the high overhead of QA and management involved.

At the time of writing this article, Module Federation is not yet available in Re.Pack. The underlying implementation needed for Module Federation support is similar to async chunks, which means that we are close to bringing it to life. In the meantime, you can start investigating other approaches, as the migration from async chunks/dynamic scripts to Module Federation will be easier in the future compared to the migration from no code splitting to Module Federation.

Future of Re.Pack

For the time being, the code splitting support in Re.Pack is in its early stage. We are focusing on addressing the missing bits needed for a stable and production-ready solution: advanced caching, security-code signing and verification, advanced fetching options, and more.
Once the foundations are laid out and solid, we are going to start working on Module Federation support.

In summary, the roots aren’t deep but the seeds are planted, so stay tuned and follow Re.Pack on its Github and Twitter profiles! You can also check out other services offered by our React Native development company.

Insights

Learn more about Super Apps

cover

Aug 31Article

Working With the React Native Super App Showcase Repository

Explore the React Native Super App Showcase architecture: Re.Pack, Module Federation, runtime bundles, shared dependencies, and production releases.

cover

Jul 21Article

A Step-By-Step Guide to Super App Development With Re.Pack 5

Learn how to connect a React Native host and mini app with Re.Pack 5 and Module Federation, then load the remote module at runtime.

cover

Jul 2Article

Mobile Module Federation with Re.Pack: When Runtime Delivery Is Worth the Complexity

Learn how Module Federation works in React Native with Re.Pack, when it makes sense, when simpler options are better, and what teams need to validate before adopting runtime delivery.

cover

May 19Article

Using TurboModule Substitution to Build Safer React Native Plugin Systems

Learn how TurboModule substitution helps React Native plugin systems provide native capabilities while preserving sandbox boundaries.

cover

Aug 27Article

Re.Pack 5.2: Faster Babel Transforms, RN 0.80 & 0.81, Rozenite

This release focuses on parallel, smarter transforms with a new Babel & SWC loader, official support for React Native 0.80 and 0.81, fully typed configs, support for Rozenite and more

cover

Jul 31Article

Running Multiple Instances of React Native in Sandbox

react-native-sandbox lets you run multiple React Native instances in one app. Build secure super apps and plugins with true runtime isolation, crash containment, and configurable permissions for native modules, perfect for integrating third-party code safely.

Event Cover

May 21Talk

Building Universal Super Apps With React & React Native

Discover how to build universal Super Apps using React and React Native. Join Mike Grabowski, CTO at Callstack, as he explores dynamic code delivery, native UI rendering, and how RePack bridges the gap with Module Federation.

Event Cover

May 20Live Stream

What’s New in Re.Pack 5.1: Live Overview

Explore runtime hooks, prefetching MF remotes, and more in Re.Pack 5.1. Join us live or watch the replay.

cover

Jan 29Podcast

Mobile Microfrontends With Zephyr Cloud and Re.Pack

Get ready for faster development cycles and reduced risks when rolling out updates for mobile apps with microfrontends enabled by Zephyr Cloud and Re.Pack.

Event Cover

Jul 8Webinar

How to Scale Teams, Product Development, and Release Process With Re.Pack

Join us to learn how Re.Pack helps manage complexity in React Native apps, enables scalable teams, and boosts release processes.

cover

Nov 7Podcast

Module Federation

In the 18th episode of the React Native Show Podcast, Łukasz Chludziński, Paweł Trysła, and Zack Jackson talk about Module Federation in Webpack and Re.Pack. They also discuss some of the buzzwords related to this type of architecture, for example, super apps and micro frontends.

cover

Jul 6Podcast

Re.Pack - Bringing Webpack to React Native

This episode explores Re.Pack, an Open Source, Webpack-based toolkit to build React Native apps with the full support of the Webpack ecosystem.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

Writing a Swift TurboModule in React Native

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.