NEWS / Simplifying Complex React Native Projects with Expo CNG and Plugins

Simplifying Complex React Native Projects with Expo CNG and Plugins

Split image with Expo logo on the left on blue background and React Native logo on the right on white background

React Native Meets Expo: A New Standard

Last year, React Native officially endorsed frameworks like Expo for development. While many have viewed Expo as suitable only for simple apps, this perception is far from accurate. The Expo ecosystem provides robust capabilities that streamline development and offer remarkable versatility - even for complex applications requiring native code modifications.

Why Expo CNG/Prebuild is a Game-Changer

Expo Custom Development Client (CNG) and the prebuild process significantly reduce project complexity. You no longer need to track ios and android folders in your repository, eliminating the burden of managing vast amounts of rarely modified native code. Instead, Expo will generate these folders when you run the project and native code changes are handled through Expo's plugin system - a declarative and trackable approach that works like infrastructure management in Terraform. Say goodbye to digging through .xcodeproj or build.gradle files to trace changes.

The best part? Anyone who can write JavaScript can write these plugins.

Step 1: Ditch app.json for app.config.js

To make full use of Expo’s dynamic capabilities, move from .app.json to app.config.js. This allows you to:

  • Pass dynamic values, such as app names, bundle IDs, splash background colors, and assets, which is perfect for white-label apps (blog post on that coming soon).
  • Write custom plugins to include or exclude packages based on specific app needs (e.g., ad-related SDKs for certain clients).

Step 2: Writing a Plugin

Example: Migrating from ReNative to Expo

For our client, we didn’t start from scratch with Expo, but instead migrated away from another build tool called ReNative. Whilst this introduced a few more challenges, such as being behind 5 versions on React Native, it did mean we could write the plugins and then compare the output of them to the old version of our repo. A great trick I used to have multiple instances of a repo checked out simultaneously was git worktree.

Code snippet as follows: git worktree add ../repo-worktree/refactor/expo

Check out more about git worktree here.

Custom Lifecycle Function in Android

For our Android project, we needed a custom lifecycle function to reduce pixel density, ensuring consistent styling across tvOS and Android. Here’s how we achieved it:

In our old project, we tracked the android folder which had the following change to the MainApplication.kt

Native Kotlin Code:

Code snippet of Native Kotlin Code as follows: override fun attachBaseContext(base: Context) {     val configuration = Configuration(base.resources.configuration)     configuration.densityDpi = configuration.densityDpi / 2      val newContext = base.createConfigurationContext(configuration)     super.attachBaseContext(newContext) }

Here’s how we do the same with expo plugins.

We start off by creating our file, we use the following boilerplate:

Boilerplate code snippet as follows: const { withMainApplication } = require('expo/config-plugins');  // Any changes to contents happen outside of exported function const addCustomCodeToMainApplication = (src) => 'newSrc';  // Naming convention for plugins: `withCustom${filename/reason} const withCustomMainApplication = (config) => {   return withMainApplication(config, (conf) => {     conf.modResults.contents = addCustomCodeToMainApplication(conf.modResults.contents);     return conf;   }); };  // Default export module.exports = withCustomMainApplication

Now we need to add our custom code. Expo has helper functions that make this much easier for us.

Expo Plugin:

Custom code snippet with Expo helper functions as follows: const { withMainApplication } = require('expo/config-plugins'); const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode');  const newsrc = `  override fun attachBaseContext(base: Context) {     val configuration = Configuration(base.resources.configuration)     configuration.densityDpi = configuration.densityDpi / 2      val newContext = base.createConfigurationContext(configuration)      super.attachBaseContext(newContext)   }`;  const addCustomCodeToMainApplication = (src) => { 	// String replace seems easy enough - however without the proper checks in place, it is easy to duplicate code changes if you don't pass the `--clean` parameter   src = src.replace(     'import android.content.res.Configuration',     'import android.content.res.Configuration\nimport android.content.Context'   );    	// The merge contents function instead handles those checks for us, a much more robust way.   src = mergeContents({     anchor: /ApplicationLifecycleDispatcher.onConfigurationChanged/,     comment: '//',     src: src,     newSrc: newsrc,     offset: 2,     tag: 'withCustomMainApplication',   }).contents;   return src; };  /**  * Halves the pixel density on Android TV devices.  * This makes styling easier and more consistent between tvOS and Android TV.  */ const withCustomMainApplication = (config) => {   return withMainApplication(config, (conf) => {     conf.modResults.contents = addCustomCodeToMainApplication(conf.modResults.contents);     return conf;   }); };  module.exports = withCustomMainApplication;

With this approach, our custom MainApplication code is tracked in GitHub, complete with a description explaining why it exists. Every time we run expo prebuild, the same input produces the same output.

All you need to do now is add the file to your app.config.js plugins array.

Code snippet as follows: ... plugins: [   './expo-plugins/withCustomMainApplication.js' ]

Step 3: Custom Plugin Behavior

Plugins become even more powerful when you start passing options to them.

For example, in a white label application you may need to exclude certain packages that aren’t required by a client. If a client has ads enabled, you can include these SDKs in the app and exclude them for those that don’t require it, removing bloat. You can pass these options via the app.config.js by passing your plugin as an array.

Code snippet showing plugin as array as follows: // app.config.js const hasAdsEnabled = clientConfig.hasAdsEnabled ... plugins: [   './expo-plugins/withCustomMainApplication.js',   ['./expo-plugins/withDynamicPackages.js', 	  { 	    includeAdsSDK: hasAdsEnabled, 	   }   ] ]

This means your with DynamicPackages.js file would look like so:

Code snippet for Dynamic Packages as follows: const modifyPodfile = (str, includeAdsSDK) => {   const anchor = 'config = use_native_modules!';    if (includeAdsSDK) {     // Make changes to the podfile to include the pod     // This means you have to exclude the implementation in `react-native.config.js`     // So that expos autolinking doesn't include it   }    return str; };  const withDynamicNativePackages = (config, { includeAdsSDK }) => {   config = withPodfile(config, conf => {     conf.modResults.contents = modifyPodfile(       conf.modResults.contents,       includeAdsSDK     );     return conf;   });    return config; };  module.exports = withDynamicNativePackages;

Now when we build a project which has ads enabled, the relevant SDKs are included. However, when they’re not needed, they’re not included. This comes with multiple benefits such as a smaller bundle size for the application, or avoiding complaints from Apple or the Google Play Store about not specifying ads are included.

Conclusion

Expo, combined with its Custom Development Client and plugin system, has transformed React Native development. Through simplified native code management, dynamic configurations, and JavaScript-based plugin extensibility, Expo has proven itself as a powerful tool for complex projects, not just basic applications.

Whether you’re migrating from another framework, fine-tuning native behavior, or optimizing app builds for specific client needs, Expo empowers developers with a clean, maintainable, and scalable workflow. It bridges the gap between native and JavaScript development, making it accessible to developers of all skill levels.

Explore More

React Native Meets Expo: A New Standard for Scalable App Development | Econify