How to Integrate multiple applications with React Native

Trãi Nguyễn
7 min readJun 2, 2023

Hello Guys, This tutorial will guide you on how to integrate multiple apps with React Native. I’ve applied it to a number of projects. Hope I can help you

There are many ways to integrate multiple apps together, with React Native
For example:

  • Integrate an existing application (native ANDROID)
  • Integrate an existing application (native IOS)

I will guide you to integrate React Native app with React Native

Photo on React Native

Table of Content

  • Ingredients
  • Create two mini-apps to connect with the super app
  • How to setup a super application to connect multiple mini apps
  • How to pass props

Ingredients

Here is a quick summary of my setup:

  • Android Studio ( Electric Eel )
  • XCode ( v14.2 )
  • ITerm
  • Nvim ( My Setup customized by Takuya )
  • Nodejs ( v16.14.2 )
  • Yarn ( v1.22.19 )
  • Npm ( v8.5.0 )

Setup a SuperApp

Need a Superapp to connect multiple applications:

npx react-native init SuperApp - template react-native-template-typescript
  • Create folder assets in Android to save bundle files of the mini app
cd $ROOT_PROJECT/android/app/src/main

mkdir assets

Create mini apps

I will create two mini-apps to connect with the super app

Go to the root directory of the super app

Create folder $ROOT_PROJECT/miniapp/*

CMD in $ROOT_PROJECT super app:

mkdir miniapp

cd miniapp

npx react-native init MiniAppOne - template react-native-template-typescript
  • Project one will create with the name MiniAppOne in the folder mini app

Edit file App.tsx from MiniAppOne :

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

const App = (props: any): JSX.Element => {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>App One</Text>
<Text style={styles.content}>
Here props from super app: {JSON.stringify(props)}
</Text>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
container: {
backgroundColor: '#ffffff',
flex: 1,
},
title: {
fontSize: 24,
color: 'red',
fontWeight: 'bold',
},
content: {
fontSize: 16,
color: 'blue',
},
});

export default App;

Run application:

Generate a file bundle of MiniAppOne 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/

Project two name is MiniAppTwo in the folder mini app

npx react-native init MiniAppTwo - template react-native-template-typescript

Same with MiniappOne

Edit file App.tsx from MiniAppTwo :

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

const App = (props: any): JSX.Element => {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>App Two</Text>
<Text style={styles.content}>
Here props from super app: {JSON.stringify(props)}
</Text>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
container: {
backgroundColor: '#ffffff',
flex: 1,
},
title: {
fontSize: 24,
color: 'red',
fontWeight: 'bold',
},
content: {
fontSize: 16,
color: 'blue',
},
});

export default App;

Run application:

Generate a file bundle of MiniAppTwo 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, we will start building SUPERAPP to connect these 2 MINIAPPs together

This is what we have prepared for SUPERAPP:

ON THE ANDROID SUPER APP

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

  • Create a folder miniapp, which includes three files ConnectNativeModule.java, ConnectNativePackage.java to operate mini-apps, and MiniAppActivity.java to handle multiple activities of Android
  • On 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" />

To the 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.

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);

// val packages: List<ReactPackage> = PackageList(application).packages
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 :


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 :

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();
}
}

Okay, that’s fine. Now, we should add some interactions from the mini app to the super app.
I will make some modifications to mini app one and bundle it again. You can do the same for mini app two.

Edit file App.tsx from MiniAppOne :

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

const {ConnectNativeModule} = NativeModules;
const App = (props: any): JSX.Element => {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>App One</Text>
<Text style={styles.content}>
Here props from super app: {JSON.stringify(props)}
</Text>
<TouchableOpacity
style={styles.button}
onPress={() => {
ConnectNativeModule?.closeApp(AppInfo.name);
}}>
<Text style={styles.content}>Close App</Text>
</TouchableOpacity>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
container: {
backgroundColor: '#ffffff',
flex: 1,
},
title: {
fontSize: 24,
color: 'red',
fontWeight: 'bold',
},
content: {
fontSize: 16,
color: 'blue',
},
button: {
borderRadius: 4,
backgroundColor: 'green',
borderWidth: StyleSheet.hairlineWidth,
padding: 20,
margin: 20,
justifyContent: 'center',
alignItems: 'center',
},
});

export default App;

And here is my super app’s App.tsx file.

/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/

import React, {useCallback, useState} from 'react';
import {
Keyboard,
KeyboardAvoidingView,
NativeModules,
Platform,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';

const {ConnectNativeModule} = NativeModules;

interface App {
bundleId: string;
appName: string;
}

function App(): JSX.Element {
const [input, setInput] = useState<string>('');
const LIST_APPS: Array<App> = [
{
bundleId: `index.${Platform.OS}-1.bundle`,
appName: 'MiniAppOne',
},
{
appName: 'MiniAppTwo',
bundleId: `index.${Platform.OS}-2.bundle`,
},
];

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

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

return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={styles.container}
keyboardVerticalOffset={64}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<SafeAreaView style={styles.container}>
<Text style={styles.title}>Input send to miniapp</Text>
<TextInput
value={input}
placeholder="please input here"
onChangeText={text => setInput(text)}
style={styles.input}
/>
<View style={styles.content}>
{LIST_APPS.map(app => (
<TouchableOpacity
key={app?.bundleId}
style={styles.btnApp}
onPress={() => goToNextApp(app)}>
<Text style={styles.appName}>{app?.appName}</Text>
</TouchableOpacity>
))}
</View>
</SafeAreaView>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
alignItems: 'center',
justifyContent: 'center',
flex: 1,
},
appName: {
fontWeight: 'bold',
fontSize: 24,
color: '#fff',
},
btnApp: {
borderWidth: 1,
backgroundColor: '#999',
padding: 16,
borderRadius: 60,
},
input: {
borderRadius: 4,
borderWidth: StyleSheet.hairlineWidth,
marginHorizontal: 16,
marginVertical: 10,
},
title: {
fontWeight: 'bold',
fontSize: 20,
padding: 20,
},
});

export default App;

This is the result for Android.
Yeah, the article has become quite long now, and I believe that for iOS, it will be similar to what we wrote earlier. If you have any questions, feel free to contact me via email. I will try to respond. If there is support from you all, I will provide instructions for iOS as well. Thank you.

--

--