Bridging the Gap : Integrating React Native Views into Your Capacitor Project

Why and when integrating RN in a Capacitor App ?

Access to specific native APIs

While Capacitor has an extensive ecosystem of plugins providing access to many native features, there are still some niche areas where React Native’s ecosystem offers unique capabilities.

Certain specialized UI components or device-specific features might have mature React Native implementations but limited or no Capacitor equivalents, AR/VR integration has been a concrete example, we dug into RN x Capacitor integration because we needed to embed ViroReact views in one of our customer app.

It’s important to note that these cases are relatively rare, as the Capacitor ecosystem, supported by both the Ionic team and community contributors, covers a wide range of native functionalities.

Before deciding to integrate React Native, thoroughly research the Capacitor plugin ecosystem, as the functionality you need might already be available without requiring the additional complexity of React Native integration.

Gradual migration from Capacitor to React-Native

If you’re planning a migration from Capacitor to React Native, a gradual approach is often the most sensible strategy.

Integrating React Native views into your existing Capacitor app can serve as a bridge during this transition. This method allows teams to leverage the strengths of both frameworks while gradually shifting their application architecture.

Performance considerations

The performance difference between React Native and web views (like those used in Capacitor) is often overstated and depends heavily on the specific use case. In many scenarios, well-optimized web views can perform just as well as React Native components.

However, for certain complex UI elements or animations, React Native might offer smoother performance due to its direct access to native UI components.

It’s crucial to benchmark and test in your specific use case rather than assuming React Native will always be faster.

The decision to use React Native views should be based on specific performance bottlenecks identified in your Capacitor app, not on general assumptions. Consider factors like the complexity of your UI, the type of interactions (e.g., complex gestures or animations), and the target devices when evaluating potential performance benefits.

Integrating React Native Views into Your Capacitor App

Let’s dive into the core steps involved in integrating React Native views into your Capacitor application.

Disclaimer:

The React Native and Capacitor ecosystems evolve rapidly. You might encounter variations, roadblocks, or errors depending on your specific versions and project dependencies. Also, this guide focuses on integrating a “raw” React Native app, as opposed to an Expo-managed one. While the general principles are similar, the specifics may differ slightly.

As a helpful guide throughout this process, keep this relevant section of the React Native documentation open in a browser tab for easy reference.

Installing dependencies

NPM

First, in your Capacitor project, you will want to add a few npm dependencies and devDependencies :

npm i -S react react-native
npm i -D @react-native/metro-config

and optional, but advised :

npm i -D @react-native/babel-preset @react-native/eslint-config @react-native/typescript-config
Android

Let’s not go too much in details here, for that you can check the commit we added in the demo repo, and check the react native docs, but in a nutshell, you will need to:

  • add the react-native gradle plugin in the dependencies of your buildscript in the root build.gradle
  • add react-native and hermes dependencies in the app/build.gradle
  • edit settings.gradle to enable autolinking and setup the react native gradle plugin
  • edit the AndroidManifest.xml application node, setting usesCleartextTraffic to true if you’re targeting API >= 28. Without this, you won’t be able to run the react-native views in debug.
<application android:usesCleartextTraffic="true" tools:targetApi="28" >
<!-- ... -->
</application>
iOS

The main thing to do for iOS is updating the Podfile to include dependencies and post install actions to make react-native work. This will at least make the running the app in debug mode work.

To create an archive (a release build) in which you can integrate RN views, you will need to add a build phase :

  • Go to the Build Phases tab in Xcode
  • Add a new build phase, by clicking “+” and then selecting “New Run Script Phase”
  • Slide it so it’s executed after the phase called “Copy Bundle Resources”

This is the content of the script you want to run at this step :

set -e

WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh"

/bin/sh -c "$WITH_ENVIRONMENT $REACT_NATIVE_XCODE"

Don’t forget to run pod install in your ios folder, and open android studio to do a gradle sync so all the dependencies you declared for android and iOS are correctly downloaded.

With the necessary dependencies in place, the next step is to incorporate the React Native app’s source code into your Capacitor project. We’ll then set up the necessary components to seamlessly load these React Native views from your TypeScript (or JavaScript) code.

Copying React Native Sources and Config Files

This step is straightforward: copy the source files from your existing React Native app into your Capacitor project. If you’re starting from scratch, create these files directly within your Capacitor project.

Here’s a minimum set of files you’ll need:

  • App.tsx (or App.jsx): This file will house your main React Native component.
  • index.js: Located at the root of your project, this file will register your main React Native component, serving as the entry point for your React Native code.
  • metro.config.js: This configuration file ensures a smooth debugging experience for your React Native views.

Remember to include any additional components, styling files, modules, or assets required for your React Native app to function correctly.

As a best practice, keep index.js at the root level. You can organize the rest of your React Native source code in a structure that suits your project.

For a detailed walkthrough, refer to our demo repository. The commit history provides a step-by-step guide.

Building the actual bridge

With your React Native sources integrated, we need to establish a bridge allowing your Capacitor app to load and interact with these views. Capacitor’s plugin system makes this process remarkably straightforward.

Here’s a breakdown of the steps involved:

Android

  1. Create a New Activity: This activity will serve as a wrapper for your React Native views. When instantiated from a plugin method, it will load and display your React Native content.
  2. Create a Plugin File: This file acts as the intermediary between your Capacitor app and native Android code. Define plugin methods that your web views can call and events the plugin can emit to send data back to your web app.

iOS

  1. Create a New ViewController: This view controller will be responsible for instantiating and managing your React Native views.
  2. Create a Plugin File: Similar to Android, this file handles communication between Capacitor and native iOS code.
  3. Update the Storyboard: Reference the newly created ViewController in your main Storyboard to ensure it’s integrated into your app’s navigation flow.

TypeScript

Register your plugin thanks to the Capacitor APIs so we can call its native methods from our JS/TS code:

  1. Create an index.ts in the sources of your project (perhaps under a “rn/plugin” folder for example)
  2. Register the plugin with… the registerPlugin method from Capacitor 🙂

Key Points:

An example of code for the Android activity and the iOS ViewController and plugins is available in our demo repo :

Calling the plugin method from the app

Once the plugins are ready, you can import the plugin in your web views, and call its methods.

import RN from 'path/to/rn/plugin';

...

async openRnView(): Promise<void> {
  await RN.openView();
}

and voilà ! Now you just need to build your sources, and make sure to run:

  • npx cap sync
  • npx react-native start to start the metro bundler
  • in another terminal you can then run npx react-native run-android and/or npx react-native run-ios to install and run the app on your device.
    • This last point is important, because running it that way will make sure the app automatically knows the URL of the metro server that is running on your machine. If you run the apps from XCode or Android Studio like you would generally do with a Capacitor app, the web views will work but the app won’t be able to load the RN ones.

Limitations and potential caveats of integrating React Native with Capacitor

Integrating React Native views into a Capacitor app, while offering unique advantages, comes with its own set of challenges. These limitations span development complexity, performance considerations, and long-term maintenance issues. Before adopting this approach, it’s crucial to understand these potential drawbacks:

Increased complexity:

  • Managing two frameworks complicates development and debugging
  • Potential for conflicting dependencies
  • More intricate build process and code organization

Performance overhead:

  • Larger app size and potentially longer startup time
  • Potentially increased memory usage

Learning curve:

  • Developers need proficiency in both Capacitor and React Native
  • Debugging across two ecosystems is more challenging

Limited community resources:

  • Fewer examples and solutions for this specific integration
  • Potential for unexpected issues due to the unique setup

Maintenance considerations:

  • Keeping both frameworks updated and compatible
  • More complex testing requirements
  • Potential difficulties in maintaining consistent user experience

Conclusion

Integrating React Native views into a Capacitor app offers a unique solution for developers seeking to leverage specific native functionalities while maintaining the benefits of web-based development. This approach, while powerful, comes with its own set of challenges and considerations