How to Develop a Super App with React Native?

Hello friends, in this article, I will explain the concept of a Super App, its advantages, and how to integrate it into a React Native application.

Alperen Yılmaz
12 min readJul 26, 2024
Super App

What is a Super App?

A Super App is a multifunctional mobile application that allows users to meet various needs through a single platform. These types of applications typically offer a range of integrated services, including messaging, social media, payment processing, shopping, food delivery, taxi booking, and more. Super Apps enable users to access many services without switching between different applications, thereby improving the user experience and saving time. These applications usually have a large user base and aim to make users’ daily lives easier.

Advantages of a Super App?

  • User-Friendly Experience: Accessing multiple services through a single application eliminates the need for users to switch between different apps, providing an easier and faster user experience.
  • Time and Efficiency: Users save time by addressing various needs on a single platform. For instance, they can manage messaging, shopping, and payment transactions through the same application.
  • Wide Range of Services: Super Apps integrate multiple services, allowing users to address many needs from one place. This simplifies users’ daily lives and provides quick access to essential services.
  • Personalized Experience: Super Apps can offer more personalized services by analyzing users’ preferences and behaviors, enabling a more efficient and effective application experience.
  • Cost Savings: By consolidating multiple services into a single application, Super Apps can help users save on various subscription fees and transaction costs.
  • Strong Ecosystem: Due to their large user base, Super Apps create an attractive platform for other service providers and partners, thereby increasing the diversity of services within the app.
  • Technological Integration: Super Apps combine various technological solutions and innovations on a single platform, allowing users to fully benefit from these technologies.

How to Integrate a Super App into a React Native Application?

In this example, we will implement a Super App architecture by integrating multiple projects into a single project. We will refer to the sub-applications within our main application as mini-apps.

First, we created a project named SuperApp, which will serve as our main application.

npx react-native init SuperApp --version 0.73

Let’s create the specified folder at the location mentioned below to store the bundle files for the mini-apps. These files will be accessed from within the main project.

cd $ROOT_PROJECT/android/app/src/main

mkdir assets

Now it’s time to create our mini-apps. Our mini-apps will be located in a folder named miniapp in the root directory of the SuperApp project.

mkdir miniapp

Next, we can enter this folder and create our first mini-project named ChatApp.

npx react-native init ChatApp --version 0.73

Edit file App.js from ChatApp,

Let’s edit the App.js file of the ChatApp project with the following code.

import React from 'react'
import { View, Text, StyleSheet } from 'react-native'

export default function App(props) {
return (
<View style={styles.container} >
<Text style={styles.title}>Chat App</Text>
<Text style={styles.content}>
Here props from Super App: {JSON.stringify(props)}
</Text>
</View>
)
}

const styles = StyleSheet.create({
container: {
gap: 10,
flex: 1,
alignItems: "center",
justifyContent: "center",
},
title: {
fontSize: 24,
color: '#03346E',
fontWeight: 'bold',
},
content: {
fontSize: 16,
color: 'grey',
},
});

GoChatAppproject root and Run application:

Generate a file bundle of ChatApp with:

npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output ../../android/app/src/main/assets/index.android-1.bundle --assets-dest ../../android/app/src/main/res/

For our second application, we created another project named ShoppingApp within the miniapp directory.

npx react-native init ShoppingApp --version 0.73

Same with ChatApp,Let’s edit the App.js file of the ShoppingApp project with the following code.

import React from 'react'
import { View, Text, StyleSheet } from 'react-native'

export default function App(props) {
return (
<View style={styles.container} >
<Text style={styles.title}>Shopping App</Text>
<Text style={styles.content}>
Here props from Super App: {JSON.stringify(props)}
</Text>
</View>
)
}

const styles = StyleSheet.create({
container: {
gap: 10,
flex: 1,
alignItems: "center",
justifyContent: "center",
},
title: {
fontSize: 24,
color: '#03346E',
fontWeight: 'bold',
},
content: {
fontSize: 16,
color: 'grey',
},
});

Generate a file bundle of ShoppingApp with:

npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output ../../android/app/src/main/assets/index.android-2.bundle --assets-dest ../../android/app/src/main/res/

Now it’s time to combine the two mini-apps we’ve created: ChatApp and ShoppingApp.

Super App Android Configuration

In folder $ROOT_PROJECT/android/app/src/main/java/com/superapp/*

  • Create a miniappfolder containing three files: ConnectNativeModule.java and ConnectNativePackage.java to manage mini-apps, and MiniAppActivity.java to handle multiple Android activities.
  • And the AndroidManifest.xml. You make added the line:
 <activity
android:name=".miniapp.MiniAppActivity"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="false" />

Super App can understand Miniapps Activity,

In the file MiniAppActivity.java, In the activity section, I have overridden the functionality of the physical back button on Android because it could potentially affect the mini apps.

package com.superapp.miniapp;

import android.os.Bundle;

import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
import com.facebook.react.PackageList;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactRootView;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.shell.MainReactPackage;
import com.superapp.BuildConfig;
import com.superapp.MainActivity;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MiniAppActivity extends ReactActivity implements DefaultHardwareBackBtnHandler {
private static MiniAppActivity mInstance;
private String mMainComponentName;
private static ReactInstanceManager mReactInstanceManager;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mInstance = this;
Bundle bundle = getIntent().getExtras();
assert bundle != null;
mMainComponentName = bundle.getString("bundleName", "");
boolean devLoad = bundle.getBoolean("devLoad");
Bundle initProps = bundle.getBundle("initProps");

ReactRootView mReactRootView = new ReactRootView(this);

String appPath = bundle.getString("appPath", "");
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setJavaScriptExecutorFactory(new HermesExecutorFactory())
.setCurrentActivity(this)
.setBundleAssetName(appPath)
.setJSMainModulePath("index")
.addPackages(getPackages())
.setUseDeveloperSupport(devLoad)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
mReactRootView.startReactApplication(mReactInstanceManager, mMainComponentName, initProps);
setContentView(mReactRootView);
}

public ArrayList<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(),
new ConnectNativePackage()
));
}

@Override
protected String getMainComponentName() {
return mMainComponentName;
}

public static void close() {
if (mInstance != null) mInstance.finish();
mInstance = null;
}

@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}

@Override
public void onBackPressed() {
if (mReactInstanceManager != null) {
mReactInstanceManager.onBackPressed();
} else {
super.onBackPressed();
}
}
}

In the file ConnectNativeModule.java,

package com.superapp.miniapp;

import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class ConnectNativeModule extends ReactContextBaseJavaModule {
private ReactApplicationContext reactContext;
private static final Map<String, ReactContext> _reactContexts = new HashMap<>();
private static Callback _closeCallback = null;

public ConnectNativeModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@ReactMethod
public void openApp(String bundleName, String appPath, ReadableMap initProps,
boolean devLoad, Callback callback) {
Intent intent = new Intent(reactContext, MiniAppActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Bundle bundle = new Bundle();
bundle.putString("bundleName", bundleName);
bundle.putString("appPath", appPath);
bundle.putBoolean("devLoad", devLoad);
bundle.putBundle("initProps", Arguments.toBundle(initProps));
intent.putExtras(bundle);
reactContext.startActivity(intent);
addBridge(bundleName);
_closeCallback = callback;
}

@ReactMethod
public void sendMessage(String bundleName, ReadableMap msg, Callback callback) {
ReactContext reactContext = _reactContexts.get(bundleName);
WritableMap result = Arguments.createMap();
if (reactContext != null) {
WritableMap map = Arguments.createMap();
map.merge(msg);
pushEvent(reactContext, "EventMessage", map);
result.putString("msg", "Send message ok!");
} else {
result.putString("msg", "Cannot find this bundle name " + bundleName);
}
callback.invoke(result);
}

@ReactMethod
public void addBridge(String bundleName) {
_reactContexts.put(bundleName, reactContext);
}

@ReactMethod
public void closeApp(String bundleName) {
if(_closeCallback == null) {
return;
}
MiniAppActivity.close();
_closeCallback = null;
_reactContexts.remove(bundleName);
}

@ReactMethod
public void getBundleNames(Promise promise) {
if (_reactContexts.keySet().toArray() != null) {
String[] bundleNames = Objects.requireNonNull(_reactContexts.keySet().toArray(new String[0]));
WritableArray arrays = Arguments.fromArray(bundleNames);
promise.resolve(arrays);
} else {
promise.reject(new Throwable("No listeners"));
}
}

private void pushEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}

@Override
public String getName() {
return "ConnectNativeModule";
}
}

In the file ConnectNativePackage.java,

package com.superapp.miniapp;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class ConnectNativePackage implements ReactPackage {

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new ConnectNativeModule(reactContext));
}

public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

Then, add the necessary steps for the connection in the MainApplication.java file.

import com.superrapp.miniapp.ConnectNativePackage; // Add Line Top of Code
 @Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();

packages.add(new ConnectNativePackage()); // Add This Line

return packages;
}

Alright, that works. Now, we need to enable interactions between the mini-apps and the super app.

We will make some changes to the ChatApp and re-bundle it. You should do the same for the ShoppingApp.

Edit file App.js from ChatApp :

import React from 'react'
import { View, Text, StyleSheet, TouchableOpacity, NativeModules } from 'react-native'
import AppInfo from './app.json'

const { ConnectNativeModule } = NativeModules;

export default function App(props) {
return (
<View style={styles.container} >
<Text style={styles.title}>Chat App</Text>
<Text style={styles.content}>
Here props from Super App: {JSON.stringify(props)}
</Text>

<TouchableOpacity
style={styles.button}
onPress={() => {
if(AppInfo?.name){
ConnectNativeModule?.closeApp(AppInfo?.name)
}
}}>
<Text style={styles.contentButton}>Close App</Text>
</TouchableOpacity>
</View>
)
}

const styles = StyleSheet.create({
container: {
gap: 10,
flex: 1,
alignItems: "center",
justifyContent: "center",
},
title: {
fontSize: 24,
color: '#03346E',
fontWeight: 'bold',
},
content: {
fontSize: 16,
color: 'grey',
},
contentButton: {
fontSize: 16,
color: 'white',
},
button: {
margin: 20,
padding: 16,
borderRadius: 4,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#03346E',
},
});

And here is our Super App’s App.js file.

import React, { useCallback, useState } from 'react'
import { View, Text, StyleSheet, TextInput, SafeAreaView, TouchableOpacity, NativeModules } from 'react-native'

const { ConnectNativeModule } = NativeModules;

export default function App() {

const [input, setInput] = useState('');

const LIST_APPS = [
{
appName: 'ChatApp',
bundleId: `index.${Platform.OS}-1.bundle`,
},
{
appName: 'ShoppingApp',
bundleId: `index.${Platform.OS}-2.bundle`,
},
];

const goToNextApp = useCallback(
async (item) => {
ConnectNativeModule?.openApp(
item.appName,
item.bundleId,
{
text: input,
},
false,
() => { },
);

const result = await ConnectNativeModule?.getBundleNames();
return result;
},
[input],
);

return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Input send to miniapp</Text>
<TextInput
value={input}
style={styles.input}
onChangeText={text => setInput(text)}
placeholder="Value to send to mini app"
/>
<View style={styles.content}>
{LIST_APPS.map(app => (
<TouchableOpacity
key={app?.bundleId}
style={styles.buttonApp}
onPress={() => goToNextApp(app)}>
<Text style={styles.appName}>{app?.appName}</Text>
</TouchableOpacity>
))}
</View>
</SafeAreaView>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
margin: 25
},
content: {
flex: 1,
gap: 10,
alignItems: 'center',
justifyContent: 'center',
},
title: {
marginBottom: 10,
fontSize: 24,
color: "#03346E",
fontWeight: 'bold',
},
appName: {
fontSize: 18,
color: '#fff',
},
buttonApp: {
width: 200,
padding: 16,
borderRadius: 12,
alignItems: "center",
backgroundColor: '#03346E',
},
input: {
padding: 12,
borderWidth: 1,
borderRadius: 8,
color: "#03346E",
borderColor: "#03346E",
},
});

Finally, Apps output,

As a result, our Android application has become a Super App with two mini-apps named Chat App and Shopping App.

Super App IOS Configuration

Now it’s time to focus on preparing the integration process of React Native on the iOS platform. We thoroughly covered the structure and architecture for the Android side, so now we need to establish this process on the iOS side and connect the mini-apps. Additionally, When integrating iOS, we will need to develop using Xcode. All the necessary steps are outlined below.

Continue with the IOS setup. We will create the files ConnectNativeModule.h, ConnectNativeModule.m, Promise.m,and Promise.hto SuperApp’s /ios directory.

First ConnectNativeModule.h,

#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface ConnectNativeModule : RCTEventEmitter <RCTBridgeModule>

@end

After, ConnectNativeModule.m,

#import "ConnectNativeModule.h"
#import <React/RCTRootView.h>
#import "Promise.h"

@implementation ConnectNativeModule

static NSMutableDictionary *emitters;
static NSMutableDictionary *promises;
static NSMutableDictionary *whiteList;

static UIViewController *vc;
static RCTResponseSenderBlock closeCallBack;

RCT_EXPORT_MODULE()

__attribute__((constructor))
static void initialize() {
if (emitters == nil) emitters = [[NSMutableDictionary alloc] init];
if (promises == nil) promises = [[NSMutableDictionary alloc] init];
}

- (NSArray<NSString *> *)supportedEvents
{
return @[@"EventMessage", @"EventRequest"];
}

RCT_EXPORT_METHOD(openApp:(NSString *)bundleName appPath:(NSString *)appPath
initProps:(NSDictionary *)initProps devLoad:(BOOL)devLoad callback:(RCTResponseSenderBlock)callback)
{
NSURL *jsCodeLocation;
if (devLoad)
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8082/index.bundle?platform=ios&dev=true&minify=false"];
else
jsCodeLocation = [NSURL URLWithString:appPath];

dispatch_async(dispatch_get_main_queue(), ^{
RCTRootView *rootView =
[[RCTRootView alloc] initWithBundleURL: jsCodeLocation
moduleName: bundleName
initialProperties: initProps
launchOptions: nil];
vc = [[UIViewController alloc] init];
[vc setModalPresentationStyle: UIModalPresentationFullScreen];
[emitters setObject: self forKey:bundleName];
vc.view = rootView;
[[UIApplication sharedApplication].delegate.window.rootViewController presentViewController:vc animated:YES completion:nil];
closeCallBack = callback;
});
}

RCT_EXPORT_METHOD(closeApp:(NSString *)bundleName )
{
@try {
dispatch_async(dispatch_get_main_queue(), ^{
[vc dismissViewControllerAnimated:YES completion:nil];
vc = nil;
closeCallBack = nil;
});
[emitters removeObjectForKey:bundleName];

}
@catch (NSException * e) {
NSLog(@"Exception: %@", e);
}
@finally {
NSLog(@"finally");
}
}

RCT_EXPORT_METHOD(addBridge:(NSString *)bundleName)
{
[emitters setObject: self forKey:bundleName];
}

RCT_EXPORT_METHOD(sendMessage:(NSString *)bundleName msg:(NSDictionary *)msg callback:(RCTResponseSenderBlock)callback)
{
RCTEventEmitter* emitter = [emitters objectForKey: bundleName];
NSMutableDictionary *result = [NSMutableDictionary new];
if (emitter) {
[emitter sendEventWithName:@"EventMessage" body:msg];
[result setObject:@"Send message ok!" forKey:@"msg"];
} else {
NSString *str = @"[sendMessage] Cannot find this bundle name ";
str = [str stringByAppendingString:bundleName];
[result setObject:str forKey:@"msg"];
}
callback(@[result]);
}

RCT_EXPORT_METHOD(replyResponse:(NSString *)requestId response: (NSDictionary *)response callback:(RCTResponseSenderBlock)callback)
{
Promise *promise = [promises objectForKey:requestId];
NSMutableDictionary *result = [NSMutableDictionary new];
if (promise) {
promise.resolve(response);
[promises removeObjectForKey:requestId];
[result setObject:@"Reply response ok!" forKey:@"msg"];
} else {
NSString *str = @"[replyResponse] Cannot find promise with id ";
str = [str stringByAppendingString:requestId];
[result setObject:str forKey:@"msg"];
}
callback(@[result]);
}

RCT_REMAP_METHOD(getBundleNames,
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
NSArray *arr = [emitters allKeys];
if (arr.count > 0) {
resolve(arr);
} else {
NSError *nsError = [NSError errorWithDomain:@"Error " code:0 userInfo:nil];
reject(@"Error", @"No listeners", nsError);
}
}

@end

And then, Promise.m,

#import "Promise.h"

@implementation Promise

-(instancetype)initWithResolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
self = [super init];
self.reject = reject;
self.resolve = resolve;
return self;
}

@end

then, Promise.h,

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface Promise: NSObject {
}

@property RCTPromiseRejectBlock reject;
@property RCTPromiseResolveBlock resolve;

- (instancetype)initWithResolve: (RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject;
@end

After adding the above files, navigate to the directories of the mini projects, then install the pods, and finally create the bundle file for the mini apps on IOS.

npx react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ../../ios/index.ios-1.bundle --assets-dest ../../ios/ --reset-cache

Then, we do the same in the ShoppingApp directory. Make sure that the bundle file name is index.ios-2.bundle.

After creating our bundle files, We need to add the file bundle generated to Xcode project super app. Click Libraries > Add Files to “SuperApp”…

After we will select index.ios-1.bundle and index.ios-2.bundle and then add.

We can run the project and the result is.

To Summarize, Using bundle files in React Native to build a Super App offers several advantages:

1. Performance Optimization

Faster Loading: Combining JavaScript files into a single bundle reduces loading times, allowing users to start the app more quickly.

Fewer HTTP Requests: Serving a single file reduces the need for multiple HTTP requests, which decreases network traffic and loading times.

2. Development Ease

Simplified Management: Grouping different mini-apps under a single project makes it easier to manage the codebase. Bundle files can be created and managed independently for each mini-app.

Hot Reloading: React Native’s hot reloading feature dynamically updates bundle files, improving the developer experience and speeding up the development process.

3. Modularity and Reusability

Modular Structure: Creating separate bundle files for each mini-app within the Super App makes the code modular and reusable. This allows independent development and maintenance of different services within the app.

Easy Integration: Adding new features or mini-apps can be seamlessly integrated into the existing bundle structure, enhancing the app’s flexibility.

4. Production Optimization

Optimized Code: Tools like Metro Bundler optimizes JavaScript code, removing unnecessary parts and reducing the bundle size. This results in a faster and more efficient app in the production environment.

Offline Support: Using offline bundle files in the production environment ensures that optimized JavaScript is available on the device without downloading, improving user experience.

5. Centralized Management

Single Platform: A Super App consolidates various services into a single platform, eliminating the need for users to switch between different apps. This enhances user experience and boosts user loyalty.

Data Sharing: Facilitates data sharing between different mini-apps, allowing for a more personalized and consistent user experience based on user behavior.

6. Expandability and Scalability

Easy Expansion: New mini-apps can be easily integrated into the existing bundle structures, allowing for straightforward expansion and scalability of the Super App.

Dynamic Updates: You can dynamically update bundle files, making changes and adding new features to different app parts.

Using bundle files in React Native to develop a Super App provides many advantages in terms of performance, modularity, production optimization, and user experience. It makes your app more efficient, flexible, and user-friendly.

Related Post:

Github: https://github.com/xayilmaz/super-app

Medium Reference:

--

--