Developing a ReactNative (RN) app for Android and iOS for the last 2 years I have more and more doubts that ReactNative is still a good choice for mobile apps these days.
Another alternative would be to write native Java for Android and objective-C / Swift for iOS and that can also be a good or bad choice depending on what you plan to do with your app. The more sophisticated your app will be regarding UI/UX and performance the more likely this can be a good choice to have more or less two apps with different development teams and maybe some shared roles, but in the end two products with the same name, but for different targets.
On the other side, the more you're focused on functionality over UI/UX and other non-functional criteria, the more likely is that a hybrid or cross-platform app is a good idea, because you only have to maintain one application. So to put that benefit into numbers, it's possible that more than 50% of development resources can be saved and that's a lot.
Before we continue I'd like to clarify the difference between hybrid and cross-platform technologies. Articles often mix them up or use them as synonyms but actually hybrid apps have some embedded browser backend like Ionic/capacitor where you can also write native plugins and utilize native APIs, which is impossible in progressive web apps (PWAs) that run in a browser sandbox.
Cross-platform apps on the other hand side use a native backend controlled by a unified runtime. E.g. ReactNative looks like the usual JSX output we have with React in the browser, but it's forwarded to an engine that mounts native UI components.
A bit of history
Mobile development really had hard times when there was more than Android and iOS and the browser wasn't well integrated. That changed over time. Actually you "only" have to support Android and iOS today and the browsers have so much power with new APIs like PWAs that you should of course even consider to use the plain browser as a suitable frontend.
Because of the reason that you have limitations in your browser sandbox it can also be difficult to make a good decision for now and the future, but good news is that around 2015 when browsers started PWA support and becoming more and more an app look and feel, there already was PhoneGap (known as Cordova framework nowadays) and Ionic/capacitor and Facebook announced ReactNative as a new cross platform mobile technology.
Today we still have PWAs in a browser sandbox with known limitations we cannot circumvent, PhoneGap/Cordova end of life was in 2020 so not a choice anymore, and we have ReactNative and Ionic/capacitor.
There is more like e.g. Flutter or Xamarin backed by Google and Microsoft, but it seems like their communities don't really grow fast enough to compete with the huge JavaScript/TypeScript community which is literally running the web front- and backend as well as being used in ReactNative or any hybrid app technologies like Ionic/capacitor. So I allow myself to question if Flutter or Xamarin should be recommended as mobile development technology today, which may of course change in the future, but for me today it's hard to believe.
That's the reason why I only see real native development for Android/iOS and JS/TS based hybrid- or cross-platform app technologies in the near and far future. With my ReactNative experiences the last 2 years and proof of concepts with Ionic I did for the current app I finally have an opinion for the technology I would use for my next mobile app. Let's start with some of the problems we faced using ReactNative.
Problems with ReactNative
Disclaimer: Some of the problems might not occur anymore or are due to reasons we caused. In the end it took a lot of time for investigation and I can't remember I had to deal with such partly nasty problems in other front-ends.
iOS HTTP2 nginx compatibility issue
The cool thing about HTTP2 is that actually the networking layer of any technology can upgrade to it at runtime if the server supports it without having to change app code so it's transparent for the app.
With a specific iOS version 14.x and higher we got strange HTTP2-protocol errors. Debugging down into ReactNative networking and reading native error logs we finally found out, that it seems like there is a problem when we send requests in parallel. Searching for it on the internet takes us to different threads where someone said it should be fixed on iOS while others say it should be fixed for nginx or forcing HTTP1. Only latter would be possible as a quick fix, but somehow experimental and as nginx is quite system critical component we decided to make iOS requests in serial for the affected versions and hopefully can remove that workaround after an official nginx version that fixes it or the iOS versions we support have it fixed.
I can't tell if that was a ReactNative issue or not, but I can't remember having such problems in the browser and the nginx version is an official AWS EKS ingress nginx.
RN updates
One of the golden rules of mobile development is that you should not wait too long for making updates. It's a ticking time bomb because in my opinion iOS and Android API development is still on a journey where they do not focus on API stabilization a lot, but make frequent breaking changes to new APIs while deprecating old API versions. That kick-off has an impact to all dependencies which also impacts your application.
That means ReactNative updates happen frequently and many dependencies especially new ones may not support yesterday's RN versions. At some point we had everything we need to implement new features without having to add new dependencies and then you should better have a different concept of keeping an eye on updates.
In our case we ran into the next nasty conceptual problem that forced us to add a new dependency which was not compatible to our current RN-version. Because there were no needs for our customer to update RN for about 20 months our update recommendations were not prioritized high enough and it was postponed to the future. But then we had to update RN and here comes the next negative criticism about RN.
We actually knew that it will not be just a small task for the afternoon, but the ReactNative update helper will run into patch problems and some manual effort is needed. Not talking about source code, asset- or dependency metadata like lock files or auto generated json assets we have:
- iOS: 1600 lines of config
- Android: 500 lines of config
And we also don't use manual linking which would blow up configuration so this is actually the raw configuration setup checked in to the code repository. Whenever there is new RN version, they provide a new empty project template and the update helper tries to patch your project. The larger the diff is through version updates, the likely it is that the patch fails. And that's what happened when we tried to jump from 0.63.3 to 0.66.3.
The diff was meaningless and so we compared the files by hand. Until that day we haven't had that much experience with iOS and Android setups. It took a lot of time, especially the iOS setup, because you have to modify and run the installation until it somehow worked. At some point one or another dependency could not build and you had to investigate why, until you have the next problem in the setup where the error message doesn't tell you anything about the actual problem, but like "cannot read /some/xconfig.rb" which looks odd as it's an absolute path, but having a look at the custom script reveals that some environment variable is not set for "${SOME_PATH}/some/xconfig.rb" and that's just one example out of many.
The good thing is, that it's more or less iterative, but we needed many, many iterations and I think almost two or three weeks to get everything fixed. Of course we also had to update npm packages because finally when we could deploy the application to our devices, it didn't start there because of an error at runtime.
In short, the iOS setup is mess of xcode project configuration no human should actually read and with custom inline bash scripts running Ruby or node.js scripts setting hidden environment variables and all in all really hard to debug. Android setup was much more easier, but also took some time.
RN "core" packages issues
The ReactNative runtime provides some of the APIs you also find in the browser and a react setup, that transforms JSX into native views, but it's lacking some of the core functionality you need in mobile apps starting with native screens and e.g. navigation gestures and ending at filesystem access. RN community is somehow fine to move that responsibility to npm packages that brings such fundamental features to the mobile app. Referring to the last section about RN updates those "core" packages are very tightly coupled to the runtime and the operating system's API that we of course also had to update them. And then one package broke our debugging setup.
It's important to be able to debug the application, but we don't need it every day. After the update journey we were so happy to consider it finally as resolved, but we were so wrong as debugging now freezes the application and it's not possible anymore. The solution was to change the underlying JavaScriptCore (JSC) engine to Hermes and use Flipper as tool.
Although it's feels like the change of the JS engine is the next upcoming nightmare it was really just a one line configuration change. Introducing Flipper was more to do, but in contrast to the RN update it was okay.
Debugging
Since ReactNative is not a hybrid app, although it feels like we have JSX and CSS, we can't use well-known debugging tools like the dev tools in the browser. It's not quite correct, because indeed the React-Native-Debugger is an Electron application simulating the runtime on the local machine and has dev-tools where you can debug network requests like in the browser, but not everything works as expected (like file uploads).
Same for debugging the UI where you can update CSS classes or styles in the browser dev-tools, but with ReactNative you only have some smaller debugging features. Either use them or default hot module reload and hack in your IDE and check your device which also sometimes doesn't work for styles. If you know what browsers can do for debugging you feel a bit far away or even lost in ReactNative, although it seems to be getting better over time like Flipper is much more better integrated for debugging.
What else if not ReactNative?
As I've mentioned before this is very opinionated, but the remaining technologies for mobile app for me is still native development or Ionic / capacitor. The decision to use one or another shouldn't be too difficult, as you can have the same technology for iOS, Android and browser apps with Ionic / capacitor, while you may want to stick back to the roots of native mobile development for maybe serious UI performance reasons or you already have native development teams with good performance.
Ionic / capacitor
Now I owe you an explanation why I always wrote "Ionic / capacitor". So capacitor is the technology that brings the browser runtime to the device, while Ionic is a set of UI components that should look like being native. So it's also possible to drop capacitor into your web app and make it a mobile app, if it has a responsive UI.
Comparing it with some disadvantages of ReactNative
- Debugging on the local machine in any browser with all browser tools is always possible and you can also use Safari for debugging your iOS application or Chrome for Android. Anyway from our ReactNative experience most of the debug sessions were not device related and within a browser runtime it's more likely not a device issue
- "core" packages are maintained by the Ionic community and backed by the company itself
- full-featured browser runtime on mobile devices using native web-view and the browser itself for desktops allows the use of the browser APIs, like fetch-API, web-worker or IndexedDB should be possible everywhere, while WebSQL is only supported on chrome based front-ends, like Chrome or Brave for desktops or the chrome-based web-view on Android, but not Firefox, Safari or iOS at all using some Safari-based web-view.
- like with ReactNative, we don't have to make a full build every time because hot module reload can update the JS source, but on the other hand a full capacitor build takes a few seconds, while our current ReactNative application needs about 2-3min for a full build and sometimes like when you have to update everything a faster build is better
That comparison is maybe a bit unfair, because it's a full-blown RN app vs. an almost empty Ionic app and of course build times are better and whatnot, but I assume it will not have the problems we had with ReactNative:
- The application is using fundamental browser APIs in the first place. That is primarily a browser DOM out of HTML, CSS and JS for the UI, which obviously never breaks. Then the fetch-API or IndexedDB which is provided by the web-view, but not implemented by capacitor also have very stable APIs.
- Gestures, animations and navigation is maybe not 100% accurate to their native UX, but Ionic is again using browser APIs and we can use well-known libraries like react-router for navigation. It's much more easier to think in URLs than in screen names and parameters.
- Finally, it's also not possible to break debugging :).
Conclusion
It's very important that as a developer we are not too far away from the code we write. That means we should be able to frequently change and verify it with almost no costs and mature technologies like the browser have so many introspection features for apps, that we may only use a few, but we're at least standing on the shoulders of giants.
I also don't see a problem, that a web application looks and feels like a mobile application and I think that "one UI for everything" is mostly a good idea and saves money. If you want to change some UI components for the desktop then simply go left or right, but not for the whole application because it's mostly not worth the time, I think.
Ionic is a very good example how to achieve that and from what I read and tested within the proof of concept, I've already recommended to switch to Ionic away from ReactNative.