Adding React Native to a Complex App — Part 1: Planning
This first article in an expert content series explores strategies for bringing React Native’s benefits to your organisation
Incorporating React Native into your organisation can be enormously beneficial. It can reduce your time to market because you can build once and deploy for many platforms. It can save you money from only needing to maintain one main codebase. It can reduce fragmentation, boost consistency and even improve product reliability and developer experience, if done right.
Incorporating React Native into your organisation can be enormously beneficial. It can reduce your time to market because you can build once and deploy for many platforms. It can save you money from only needing to maintain one main codebase. It can reduce fragmentation, boost consistency and even improve product reliability and developer experience, if done right.
Many organisations make React Native adoption a cornerstone of their transition to being a modern digital organisation . Thousands of apps already use React Native in production, including many Fortune 500 companies and dozens of household names. What links Microsoft Office and Messenger? Or Playstation and Pinterest, Baidu and Bloomberg, Wix and Walmart? They all use React Native.
But migrating complex apps to React Native is, well, complex. It requires serious planning, world-class expertise and genuine commitment. Organisations almost always need expert guidance and support. At NearForm, our clients report reductions in time to market of up to 40% thanks to our cross-platform app development expertise . But success isn’t guaranteed. Many companies have tried and struggled or failed, and migrating complex existing apps is especially challenging.
We have created a series of articles to outline some of the key principles and pitfalls a company should know about to get it right:
Part I (this article) focuses on the most important but most easily overlooked step: planning.
Part II goes ‘under the hood’, exploring some of React Native’s ‘black box’ internals.
Part III goes deep on technical details when adding React Native to an existing Android app
Part IV goes similarly deep on adding React Native to existing apps in iOS
Let’s get started, with part I: planning .
Choosing the best approach: greenfield or brownfield?
Cross-platform mobile app technology like React Native can greatly increase productivity and product consistency, but for companies with one or more established, complex, real-world-proven apps that have been through years of refinement and iteration, the idea of starting from scratch can be daunting.
Greenfield apps
A ‘greenfield’ app rebuild (starting from scratch in the new technology while the live app is given essential maintenance only) is the simplest and most efficient way to work, but some organisations see the timelines to reach production as prohibitory. Nothing can go live until everything can go live: all existing features need to be recreated and given rigorous quality assurance testing, to ensure that the clean and slick new app doesn’t lack subtle but crucial details that evolved over time in the old app.
Greenfield projects are much more technically straightforward, and are the clear best option if existing apps are simple, or have significant quality issues such that there’s little risk of losing wheat with the chaff, or if there’s little time pressure to get new features into production. The specific challenges focused on in this series are generally unique to the more challenging brownfield app projects.
Brownfield apps
A popular way to bring new technology to market faster is a ‘brownfield’ app, where React Native is integrated into the existing app. The team can then build and ship new features in React Native that sit alongside existing ‘legacy’ features that remain largely unchanged until it’s their turn to be modernised and replaced, one at a time. It’s easy to see why this is so tempting:
The productivity boost for building new features is achieved sooner
New features can be shipped to real users sooner
Incremental changes are less disruptive to users
It removes some of the risks of a long slog and then a ‘big bang’ relaunch
React Native in brownfield projects
React Native is the natural choice for such a project, because, unlike most alternatives, the actual app screens are made of the same real native elements as everything else in your app.
React Native’s cross-platform brain is designed to sit in and drive a regular native app. This distinguishes it from platforms like Flutter and Xamarin that bypass the conventional app structure, and ‘hybrid apps’ like Ionic and Cordova that imitate native apps from inside noticeably-different HTML web views.
Some people can be confused by React’s popularity on the web into thinking that React Native apps are also HTML ‘hybrid apps’. They’re not. React Native’s controller follows the same patterns as React, which brings the additional benefit that many layouts, logic, components and features can be made reusable for the web, but within an app, React Native uses 100% real app elements.
A high-quality, well-planned brownfield React Native app can therefore be almost seamless. It’s possible to make a user’s transition from a legacy app feature to one driven by React Native as imperceptible as when a pilot hands over to their copilot.
However, we need to really emphasise those caveats: “high-quality” AND “well-planned” . Making a brownfield React Native migration work well is a very significant technical and organisational challenge, and one with many potential pitfalls to anticipate and avoid.
Essentials
Before planning the technical engineering work, a number of things should be in place or planned for to make it possible.
Managing expectations
A common phrase you’ll hear from people who have worked on brownfield React Native projects is, “It gets worse before it gets better”. This can be moderated to some extent with careful planning. However, there’s no getting around the fact that, by their very nature, these projects aim for long-term efficiency gains via short-term complexity pains.
It’s not as simple as it might look
React Native’s documentation for integrating existing apps looks straightforward. The steps appear easy to follow, and they fit the general pattern of wiring in a module or SDK much like any other. It all looks easy… until very suddenly it isn’t.
React Native makes a number of assumptions about the app it sits inside: in particular, that it is both modern (but not bleeding-edge modern) and simple (but with certain expected configurations). However, real-life ‘brownfield’ React Native projects involve complex apps with years of accumulated technical debt that may have taken less conventional paths. If the apps were as simple as assumed, they could easily be rewritten as ‘greenfield’ apps, and wouldn’t need this intermediate step where pure native and React Native coexist.
Expect the unexpected
React Native’s requirements aren’t very strict, and some projects may get lucky and encounter almost no friction here, but it is likely that there will be more unexpected issues, delays, false starts and required compromises than in a typical software project.
It’s also almost certain that the developers who need to resolve these issues will encounter them while working outside of their normal comfort zones and core skill sets.
Skills and resourcing
The people challenges characteristic of brownfield project may be even more tricky and delicate than the technical challenges: you’ll rely on rare skills intersections including the in-house expertise of the people who, if you’re not careful, may be the most disrupted and alienated by the project.
You’ll need expertise spanning your apps and React Native internals
The most difficult work in the migration process will be wiring in React Native to areas of your existing apps that are complex, idiosyncratic or unconventional. Resolving these will require above-average expertise on both sides of the intersection, but you can’t magic up one person who is an expert on the quirks of your Android app and your iOS app and React Native at the same time.
Find internal experts who are able and willing to learn React Native, and pair them with experienced React Native developers or consultants who have baseline skills in native code.
Writing React Native code is very different to writing Android and iOS code
Learning React Native will require the people in your in-house team to step out of their comfort zones. It’s not just different languages, the whole approach is different (functional vs object-oriented, interpreted vs compiled). Well-planned training and good technical leadership will be crucial.
In particular, reduce culture shock by investing some time in TypeScript and IDE configurations that are comfortable for developers used to strict type safety within purpose-built IDEs, and boost motivation with some focus on areas where the React Native developer experience is an improvement on native apps, such as:
Smoother UI iteration, via ‘hot reloading’ in development and over-the-air updates in staging, allowing changes to become visible almost instantly instead of requiring app rebuilds
Easier separation of concerns and unit testing, via business logic managed in simple pure functions instead of complex deep-nested subclasses
Better debugging workflows for runtime errors, via integrated debugging tools like React Native Flipper and full stack traces in Hermes
Developers will have personal concerns
It’s only natural that some individuals may see change and new technology as a threat, and this needs to be carefully managed on a personal level. Some may also struggle with the change from feeling like a big fish in a small pond, making large contributions to one app, to feeling like a smaller part of a bigger team. The team overall will be more productive as they build for multiple platforms, but individuals may feel like a smaller part of it.
Be patient, and be prepared for pushback and growing pains.
Sometimes, some code can and should stay native
The existing native apps won’t be simply junked and your team’s existing skills and knowledge won’t become wholly obsolete: as well as the lengthy transition period, some elements of existing apps may be uniquely complex, sophisticated or platform-specific, and one of the benefits of React Native over rival cross-platform systems is that these can be repackaged as native modules. This unique, battle-tested code can continue in its current form, as a platform-optimised component of a cross-platform app.
Identify if there are any such areas in your apps, and ensure that necessary skills and knowledge are retained.
Testing and quality assurance
Robust testing and quality assurance is always important in any app project. Hopefully, the apps that are going to be migrated already have robust QA processes and automated test suites, including end-to-end testing of a complete build — but often, React Native migrations are part of digital modernisation programmes where the existing apps haven’t followed best practices.
Smoke tests
At a bare minimum, you’ll need automated smoke tests that catch unexpected breakages in key user journeys and features, especially any features that depend on other integrations like third-party SDKs. Adding React Native shouldn’t compromise or change any existing features, but it has certain build configuration requirements, and changing build configuration always carries a small risk of unexpected side effects. It’s important to know that if something did change or break in such an unexpected way, this would be caught.
It’s also important to ensure a wide range of devices are tested with at least ‘does the app start’ smoke tests. React Native loads some device-specific binaries at run time, meaning that for non-standard integrations, there’s a slim risk of device-specific crashes if a misconfiguration caused a binary needed by an unusual device type to be missing.
At a bare minimum, set up a basic QA system, including automated and manual testing, where every major feature is tested on a wide and market-appropriate range of devices before each release.
End-to-end tests
Migrating features to new technology naturally brings a much higher risk of regressions. E2E tools that run on a complete app build, like Appium and Maestro, can be used equally well for React Native and pure native features. It is extremely advisable to fill in any gaps in E2E tests for each feature or screen before it is migrated: if the migration is done well, these tests should pass with no changes needed except for planned refinements or modernisation, confirming that the core behaviours and interactions have remained the same.
If possible, have a robust CI/CD pipeline running E2E tests, where each screen and feature has comprehensive coverage before beginning its migration.
Tools specific to React Native
Features created in React Native can benefit both from this existing E2E tooling, and more focused dedicated tools designed for React Native. These include linters that catch mistakes and anti-patterns as they’re written, and unit tests and component snapshot tests that can catch errors and regressions in logic or layout before code changes are even committed, without needing a build.
Set up tools like Husky, React Native Testing Library, and ESLint with plugins for React Native, React hooks and React Native accessibility from the start.
Viable strategies for adding React Native to a complex app
There are lots of different ways mixing React Native and native screens in an app can be managed, but they mainly boil down to three:
1. React Native shell
This is probably the best long-term option. This Shopify case study describes in detail how this strategy can successfully work at scale. In summary:
Start by migrating the main app shell and main navigation to React Native. This is a substantial task, but doing it first means that:
The React Native modules will start in the main app boot process (main app Activity on Android), so there are no extra delays when the user moves from a native screen to a React Native one
Every React Native screen will be plugged into one main React context. They can be developed almost as simply as in a conventional React Native app, avoiding the problem faced by many 'brownfield' React Native projects of screens and features being isolated in contextual silos
Set up native screens to be handled by the React Native navigation system as routes, the same way React Native screens are
With all this in place, native screens can be migrated to React Native relatively simply, one at a time
2. Native shell, React Native fragments
This is a quicker but dirtier option — it’s faster to add visible value, but accumulates more complexity and has many potential pitfalls. CallStack / Kiwi.com have a case study discussing mitigating some of those pitfalls. In summary:
React Native screens are added to the native apps as Android Fragments and iOS View controllers
The native app navigation is tweaked to navigate to these elements in its usual way, slotting them inside the native app shell layout. As a result:
The native app and navigation barely change at first, reducing delays at first
Every new React Native feature needs to be hand-stitched into the native navigation, causing accumulating complexity
Each React Native feature and screen is somewhat isolated from others, and depends on the native app and custom native modules for any global state-
For best results, manually boot up React Native JavaScript during app load (even if it is not needed yet) and ensure it is persisted in memory, to avoid users hitting unexpected slow transitions later, and add overrides to handle system navigation events like Android back button and iOS back-swipe.
3. Native shell, React Native activities
This is quite simply the worst of both worlds. A well-planned project shouldn’t use this strategy, but there’s a risk of ending up here by accident if the React Native documentation is followed as written without proper planning. In summary:
It is largely the same as option 2 above — except on Android, where React Native screens and features are added as Android Activities (instead of Fragments within native Activities).
Navigation into React Native screens on Android done this way will be extremely clunky. The area controlled by React Native will also be inherently full-screen, covering native tabs, drawers, modals and alerts. This is almost always undesirable, unless for some unusual reason it is desirable for the app to contain multiple distinct full-screen 'sub-apps'.
When to use each strategy
Option 1 (React Native shell) is an advisable long-term option. This is because it provides a clear, scalable foundation where each new migration of a screen or feature to React Native reduces the complexity and technical debt on the native side. It shows a clear pathway for migrating to React Native long term, and once the React Native shell architecture is built, each migration reduces overall complexity and technical debt.
Option 2 (Native shell, React Native fragments) is an advisable option for short-term projects. These include ‘Proof of Concept’ (PoC) apps not intended for production, or legacy apps approaching end-of-life where only a small number of React Native features are intended to be added or where there isn’t a plan to migrate the rest of the app.
It is the fastest way to ship a working React Native feature, with fewer changes (therefore fewer possible regressions) to the native app. However, it is not an ideal pathway for long-term complete migration of an app to React Native. This is because each new React Native screen and feature accumulates complexity and technical debt on both the native and React Native side.
One strength it does have, is that a PoC app produced via option 2 can be a powerful tool for demonstrating the speed and power of React Native, which can be valuable for getting buy-in for a bigger project from unsure stakeholders. While the PoC itself isn’t an ideal foundation for further development, its build configuration will be reusable, and this is one of the most difficult parts of a brownfield React Native project, so effort is not wasted even if the specific PoC features are never pushed to production.
When to mix strategies
Here are a couple of possible mixed strategies for getting the best of both worlds:
A: Throwaway PoC, keep the build config, add React Native shell
This is a medium-risk and medium-pace option that may be advisable if:
Internal decision-making is complex and stakeholders want to see the potential benefits for themselves before committing to a major project, or
Robust long-term migration is more of a priority for the project than developing and shipping specific new React Native features, or
There is a technical team available without a very sharp line between ‘native-oriented’ and ‘React-oriented’ developers (e.g. specialist React Native consultants), or, specialists can be pulled in as needed (e.g. from a partner consultancy with diverse skills)
Process:
1: Start with a Proof of Concept following the ‘Option Native shell, React Native fragments’ approach, with a clear mutual understanding that:
The build configuration and setup is the most complex and expensive part of the PoC, and will be kept and re-used for the actual project
The React Native feature itself is the simplest and cheapest part of the Proof of Concept, and will be a throwaway PoC only
2: Use the working Proof of Concept app as a learning and internal communication tool. Demonstrate to stakeholders how quickly and easily feature changes can be made within the React Native feature, and use it as a training tool to kickstart React Native upskilling of native developers
3: Build an ‘Option 1’ React Native shell on top of the working ‘Option 2’ build configuration and project, rebuilding the PoC React Native feature as part of what is now a React Native app where native legacy screens are managed by React Native navigation.
4: Once the shell is built and regression-tested, the team is ready to focus on bringing new features to production built on React Native, with the foundations necessary to ensure that the difference can be made largely invisible to the user.
Strengths:
Technical viability is proved early
The step with the highest risk of a deal-breaking problem is dealt with as early as possible
There’s one clear workstream and unified team goal at each stage
Foundations are made stable before building more on them
Weaknesses:
- The most complex and challenging native-oriented work (build configuration and React Native shell) is all front-loaded and needs to be completed before the first production-ready React Native feature can be demonstrated
- During this first phase, project progress will be entirely ‘under the hood’ and may be difficult to assess by external stakeholders
- There’s a risk React-oriented React Native developers may be underutilised or blocked during the early stages, when the main focus is on native-side integration work
B: Isolated React Native shell built parallel to native integration
This alternative takes several small risks to pull some of the React Native frontend work earlier in the project. It is worth considering if:
There are high-priority new features intended to be shipped in React Native which will need to go through a lengthy internal review process, or
The priority of internal stakeholders is to see visible progress on new features, or
The team has a clear split between ‘native-oriented’ and ‘React-oriented’ developers who will be present from the start
Process:
1. Set up a pure React Native app, where the React-oriented team members can build the main app shell and navigation, then start working on those aspects of the high-priority new features that have few dependencies on the native app.
2. Meanwhile, in parallel, the native-oriented side of the team works on build configuration and integrating a minimal React Native “hello world” screen into the native app, with nothing added beyond a decision on whether to use “New Architecture” with Hermes and Fabric (more on that in Part II ), and bare installs of required React Native modules that have native-side requirements: typically this may include react-native-screens
, react-native-reanimated
, react-native-gesture-handler
, react-native-svg
, a navigation library (for brownfield projects, the more native-oriented react-native-navigation
is likely a better choice than the more popular @react-navigation
), and any React Native wrapper modules for business-specific SDKs you use.
3. As soon as the native apps are building successfully with React Native integrated, the native-oriented team pivot towards merging the workstreams: weaving the existing native screens, services and API into the new React Native shell
Strengths:
- Visible progress on high-priority features enables early input, user acceptance testing and iteration
- Full utilisation of React-oriented developers at all stages of the project
Weaknesses:
- The time for completing React Native integration into the native app is slower (increasing the risk of high-impact complications arriving later)
- There’s a risk of conflicts when merging the two separate work streams
- There’s a risk of some React Native work needing to be reworked if it is built on bad assumptions (for example, about how existing native app features like auth tokens may be accessed)
- Harder to manage due to two parallel workstreams
Conclusion
Adding React Native to an existing, complex app can yield excellent benefits and results for businesses, developers and users. However, while the rewards are significant, undertaking and achieving this presents a significant challenge to technical and management teams that should not be underestimated.
Careful planning is essential, as is ensuring that experts are available who can handle the technical challenges. This is something NearForm can help you with. If you’d like to discuss how we can support your organisation in adopting React Native then get in touch . Our experts would love to chat about how we can help.
In part II , we will look at some of the specific technical challenges that come with adding React Native to a complex app.
Insight, imagination and expertly engineered solutions to accelerate and sustain progress.
Contact