React Native: A slightly advanced guide
React Native will be your go-to framework for app development! Unless you’re building a CPU/GPU-intensive game that requires cranking 60 frames out per second without fail.
Unless you’re building a CPU/GPU-intensive game that requires cranking 60 frames out per second without fail, chances are that React Native will be your go-to framework for app development!
React Native is an obvious success story for the major advantages it brings to the table - support across major platforms, purely native elements, a single codebase, over-the-air updates, active community, abundant learning resources and the list goes on! React, and Javascript play a pivotal role in this story - To lower the entry barrier, web developers can easily transfer their skills. And to sweeten the deal even more - a good chunk of React and Javascript libraries can be simply reused in React Native. This puts React Native easily ahead of competing frameworks 😄.
Having said that, the learning curve is pretty steep for novice engineers. React Native has a layered architecture and can be daunting at first glance. Learning Javascript, framework APIs and state management techniques is the first step. In the real world, you’ll encounter exciting problems that tutorials and guides don’t usually cover. I hope this article will encourage newcomers to get their hands dirty and go beyond basic tutorials. I also hope to set realistic expectations around learning velocity, curve and complexity.
Let’s imagine that you’re working on a shiny new app at an emerging startup! But you’re totally new to React Native and its quirks. What are some of the things you should know before shipping the app? We’ll find out soon. But here’s the index in case you want to jump around -
Language and framework APIs
Alright, you’ve already started with a “Hello World” app and trying to understand what’s happening - going well. The trick to learning any new framework is to start playing with it. Once you get the hang of it, you’ll realise that you need to know more than just framework APIs to write production-grade code.
This is where knowing the underlying language plays a significant role. There are practically unlimited resources to learn modern Javascript and React. And feeling lost is a totally valid reaction. Wouldn’t it be nice to know where and what to look for? Say no more 😎
javascript.info - I highly recommend this excellent refresher course to everyone. MDN is also an incredible resource.
The good parts - This book helped me wrap my head around confusing concepts of Javascript. If you want to go that extra mile, give it a go!
Read all resources you can on functional programming. It’ll give you a totally different programming approach. Nerd out with Thinking Functionally series… or not. Up to you!
ES6 and onwards - Just google for es6 and es7 features. You may not need all of them, e.g. Generator functions. But you’ll absolutely need some of them - Eg. Spread operator.
Learn a bit about transpiling as well. Babel and Typescript are the most popular tools.
Finally, React and React Native - Any tutorial available on YouTube will be good enough. Do not buy any paid courses. You’ll notice that your efforts of learning Javascript and ES6+ features are really paying off.
I’d also like to make a special mention of 2ality blog for a deep dive. It’s just too good.
💡 Key takeaway: Language and framework APIs will require considerable investment of time (weeks or sometimes months) but it will pay the highest dividend in the end. But the flip side is - Beyond basics, don’t get too worked up with the language. You’ll learn many things on the go.
Typescript
The popularity of Typescript has surged tremendously in recent years. According to Stackoverflow Developer Survey, Typescript was the 7th most popular language in 2021. It only improved in 2022 and grabbed 5th place! 🚀
Long story short - JS has a very loose concept of “types”. This leads to bugs and maintenance nightmares as the codebase gets bigger. Typescript makes refactoring ten folds easier, and confidence skyrockets when shipping the product. This is invaluable for teams that ship critical code. But it’s worth mentioning that you can do without Typescript. It doesn’t give you any edge over other candidates for entry-level roles.
💪🏻 If you feel like giving up at this stage, know that it’s totally understandable. It’s a lot of grunt-work. But if you can survive this initial storm of fundamental concepts, I promise you that the rest of the path will be joyful and interesting!
Animations and gestures
So you’ve built out a few features by now. But you want to spice it up with some animations! 🌶️
User engagement is an essential metric in any app’s success story. Even the most annoying errors can be handled gracefully with a delightful user experience. And the animation is a big part of that delight. Swiggy, CRED, Spotify and many other apps make clever use of it, and boy, do we love these apps!
Think of animation as physics simulated on the screen. The inertia and momentum of objects have to be believable. Interpolation, timing, and curve functions are some of the basic building blocks. And so, there’s a fair bit of maths involved if you dive deep into this. Fortunately, there are great libraries out there that make the job easy.
Animations are necessary, but they can also be hard to do right. One way is to write a state transition logic to animate the position of an element. But it runs on a Javascript thread which is not built for animations. Instead, React Native provides a particular Animated API that runs on the UI thread and renders smoother animations.
🔥 Pro tip: If you’re using the
Animated
API, always passuseNativeDriver: true
property for performant animations. Using native driver will move the computation on native side. React Native has a dedicated blog post to explain this.
https://miro.medium.com/max/1280/0*-P5HQcnHkN5e1aYU
For slightly complex operations, it’s advisable to use third-party libraries and leverage their optimized code and polished animations.
LottieFiles is a low-code tool for animations in React Native. A LottieFile is a JSON file that has the animation configuration. All it needs is a <Lottie />
component to run the animation.
For more customizability, consider using react-native-reanimated or react-native-animatable libraries.
If you’ve used Framer Motion on the web, you might like moti. Their API is declarative and easier to use.
For gesture handling, react-native-gesture-handler is by far the best library or just use the vanilla API.
Animations are a common cause of performance degradation. So use it sparingly 😉
Bonus
I enjoyed watching this video where the author will walk you through some cool interactions and animations. You’ll also find this series of blog posts interesting on simple physics using react-native-reanimated.
Instrumentation
Fancy animations added ✅ Are we ready to ship now? Well, not quite. Before publishing the app, we’d want to put some mechanism in place to know how the app performs in the real world. For that, a little bit of plumbing is required. Surprisingly and anecdotally, instrumentation, aka telemetry, is a less discussed topic in the community. But its importance cannot be overstated. Collecting user feedback, observing user behaviour, running A/B tests and gathering usage statistics is crucial for any product’s success.
In the context of mobile apps, telemetry helps product owners capture data on how users engage with the digital product and developers to capture the current health of the app. If the app transmits valuable information, many questions can be answered by visualizing the data.
Product owners will want to know.
“How many users actually placed an order after adding items to the cart?”
“How many users noticed the shiny new button and clicked on it?”
“What’s the average time a user spends to place an order?”
App developers will want to know.
“How many seconds does it take for the app to start?”
“How many users are using older versions of the app?”
“What was the stack trace when the app crashed?”
FullStory, Mixpanel, and Google Analytics are some popular service providers. They come with the ability to crunch petabytes of data, so there’s virtually no limit to how much data you’re sending out. In many cases, multiple providers are used to covering different use cases.
🔥 Pro tip: As a programmer, you’ll want hide implementation details of tracking from individual React components in your app. Details such as which platforms are used, how the methods are called and what is the shape of data being transmitted. Here, libraries like react-tracking come in handy. It provides a nice abstraction that prevents leaking details across the app and easy replacement of service providers if you wish.
Native modules and libraries
React Native allows you to use the native capabilities of the device, such as SMS, Bluetooth, Wifi, Location and much more. Meet “Native Module” - it’s a thing with 3 layers.
Native modules can be hosted inside android
or ios
folder in your source code and consumed directly. But if you’re planning to cater to a larger audience, say inside your org or with the open source community, you can create a library too.
Thanks to the vast React Native community, almost everything you need to build the app is already packaged and available on NPM and Github. So creating a new package can be redundant. But if you find an opportunity to create one, you’ll find the react-native-builder-bob tool very useful to scaffold React Native libraries. This tool is a one-stop shop for everything related to the creation of libraries.
For our amusement, let’s consider you want to turn on the Bluetooth and search for available devices around you. You can create a native module called MyBluetoothModule
in Java/Kotlin or Objective-C/Swift and invoke its methods from Javascript.
There are 2 sides to every native module - native code and Javascript code that invokes it. This creates an exciting twist while releasing. Let’s see what it is.
Valuable resources: Official documentation and YouTube playlist.
Distribution: Native and over-the-air
With all that in place, your app is ready for the much-anticipated launch! 🚀
Mobile app distribution is an exciting journey. React Native targets Android and iOS by default. Microsoft has been working on a framework for Windows and macOS apps too. But for brevity’s sake, let’s stick to just mobile platforms.
The final outcome of React Native apps are APK (Android) and IPA (iOS) files; let’s call them “artefacts”. We upload them to Play Store and App Store for publishing. The generated artefacts must be “signed” by you or your organization before they can be uploaded. The official docs cover all the steps thoroughly. Check out the iOS guide and Android guide.
Note that Google has a Play Console, and Apple has App Store Connect, which are used for distribution, testing, app review and general administration of your app.
💡 I’d recommend creating developer accounts for both iOS and Android to get a first hand experience of publishing. It may cost you a little bit. But it’s totally worth it. Afterall, the fun lies in getting your hands dirty and failing a few times along the way 😉
Both platforms offer “testing tracks”. Tracks are super helpful for running automated and manual tests on the app before it goes live.
Both consoles support the phased rollout of the app. You can control the percentage of users that receive new native updates. This is a valuable safeguard against faulty artefacts being deployed. You can gradually increase it to 100% as you gain confidence in the deployed artefact.
Over-the-air updates
OTA is a USP of React Native. Let’s break down what it means and why it is so important. A bit deeper into how the artefacts are created and how updates are installed by the users will explain the need for OTA updates.
The diagram describes how different components of the artefact are updated differently. This is specific to Android. A very similar architecture is employed for iOS apps as well.
OTA enables us to push out updates without having users re-install the app. This is awesome for product owners! It allows fast iterations and quicker bug fixes.
OTA in React Native roughly translates to downloading and replacing the Javascript bundle to oversimplify. Microsoft offers AppCenter, aka Codepush, to orchestrate automatic bundle publishing. You should definitely check it out!
Gotcha!
There’s a catch, though. And it’s a nasty one 🙃 As long as you only deal with Javascript, OTA updates will work fine. But if upgrading a Javascript package or Javascript code that internally calls a native module, you must also ensure that the corresponding native code exists in the APK.
In other words, the OTA release only updates Javascript on the user’s device. The native code still needs a regular release. OTA release is adopted faster than native release. So while users are using the latest Javascript bundle, they might not have the corresponding native code on their device.
To mitigate this problem, you must add a lot of defensive code, i.e. null checks, in your Javascript bundle. Let’s take a look at this pseudo-code to illustrate the problem.
You’ve created a new Native Module called MyBluetoothModule
. Before invoking it with Javascript, you have to add a null check.
import { NativeModules } from 'react-native';
const myBluetoothModule = NativeModules['MyBluetoothModule'];
if (myBluetoothModule) {
myBluetoothModule.turnOn();
const devices = myBluetoothModule.searchDevices();
Logger.log('Devices: ', devices);
} else {
Logger.log('Bluetooth: Native module not found. Skipping search');
}
When myBluetoothModule
is null at run-time, know that the device does not have the latest APK. You’ll have to maintain this code until 100% of users use compatible Javascript bundle and native code.
Profiling and benchmarking
Your app is in the wild now! And users love it! As it grows in size and complexity, it’ll eventually end up facing performance bottlenecks ⚠️
Identifying what’s causing the problem requires special tooling. Inspecting different metrics of your app at run-time can reveal underlying problems. Profiling tools come built-in with React Native, Android Studio and Xcode.
Some of the problems can be hard to track down - unresponsive UI for seemingly no reason, intermittent OutOfMemory
errors followed by crashes, unexpected battery consumption etc. Looking at the code may not fully describe the problem. In such cases, you’ll want to go one level deeper and inspect the execution and impact of your code on the system.
To begin with and to not get intimated by all the tooling, wrapping a code block with Date.now()
measurements to compute the time spent executing a block of code is a very solid technique to find bottlenecks. Once you’ve found the right spot, these tools can help you dive deeper.
const now = Date.now();
runSomeExpensiveOperation();
const then = Date.now();
if (then - now > 1000) {
// look closer into runSomeExpensiveOperation(). It's making the app slower.
}
Flipper is an excellent debugger for everyday use cases. It comes pre-configured for apps created with React Native 0.62 template. For older templates, manual configuration is possible. Do watch the introduction.
React devtools is a handy little tool that inspects the component trees, state, props etc.
To be honest, profiling deserves a blog of its own. Ain’t nobody got time for that! 😛 But the official docs do a good job of introducing profiling tools. I’ve also hand-picked blogs for you to get started - One for Android and one for iOS.
The Android team has created a terrific video series on profiling. Do check out the Live Q&A video at the end for additional insights.
Benchmarking
Performance is better expressed in relative terms. You need baseline numbers to understand whether your app has improved or regressed. This is called benchmarking. A naive approach to benchmarking can be running your app 10 times, recording metrics and then averaging out. The numbers can be memory consumption in MBs, battery usage in %, startup time in milliseconds, storage read/write time in milliseconds etc. This will be your baseline. In the next section, let’s look at a few techniques for improving your baseline.
🚨 Important: Profiling and benchmarking should be performed only on real devices and on release mode artifacts (APK and IPA). The reason being - emulators are essentially using powerful CPUs of your development machine and debug mode artifacts carry overheads that are completely absent in release mode. So results will drastically vary in real device vs. emulator and debug vs. release mode artifacts.
Performance
App performance is a vast area. There are many facets and trade-offs to it. Users want apps to launch quickly, render smoothly, and require little memory and battery usage. But it's easier said than done.
As we established in the previous section, benchmarks should be our starting point. Some low-hanging fruits, such as the Hermes engine, may instantly improve some numbers, but generally, the improvements are small, incremental and painstaking. The numbers vary because of the variance in physical devices. For, e.g. iPhone users tend to have a smoother experience because of the superior device quality. But users with low-end devices (lower memory and slower CPU) will complain about the app's performance. We should always strive to optimize for the bottom-end users because they comprise most of the user base.
One way to optimize can be to target different layers and improve them one by one and measure the improvement in metrics. E.g. Use proper memorization techniques in React and observe if there’s any drop in the number of re-renders and increase in FPS.
Another way to look at it is to target different performance metrics by optimizing all associated framework layers. E.g. Target the “App startup time” metric and improve it by optimizing all app layers.
Both approaches have their pros and cons. The takeaway is that the end goal should permanently be established in advance. E.g. Reduce app size by X%. Otherwise, there’s a risk of expending efforts with no apparent results.
As usual, the official docs of Android, iOS, React Native and React are sufficient to get started on your performance improvement journey. But here are some key considerations for you.
React: Always avoid expensive re-renders. Proper use of memoization, immutable structures, and lifecycle hooks such as
shouldComponentUpdate
(to avoid unwanted re-renders) andcomponentWillUnmount
(for avoiding memory leaks) is the way to start. You can use the built-in Profiler to observe re-renders in the component tree. Flipside - don’t memoize too much.React Native: Using the Hermes engine may give you instant improvements in startup time, decreased memory usage and smaller app size. If you’re building a custom List/Scrolling component, make sure it’s virtualized. Animations should be run on the UI thread and not the JS thread.
Native libraries: Inspect native libraries that get loaded at the startup. You may find some of them are not required at all! With the current architecture, all 3rd party native libraries are loaded at once during the startup. Clean up unused libraries, and replace slow libraries with faster ones to improve startup time.
App size: The size of your app impacts app load time, memory usage and power it consumes. Android and iOS provide detailed instructions to reduce the artefact size. They involve obfuscating code, reducing asset size (images, audio and video files), removing dead code and unused libraries etc.
App startup: Improving app startup time is about avoiding heavy code initialisation and optimizing assets and network requests. Faster app loads translate to better user retention, engagement and overall satisfaction.
💡 Performance optimization is a tedious job. If this feels too overwhelming, be assured that it is. Be okay with that feeling. Break down problems into smaller ones. For eg. If users are complaining of slowness, try to dig in more. Is the app startup slow? Is the UI lagging? Or is there a particular screen that’s slow? Questions like these will help you narrow down the problem.
Pick any one metric to improve, you’ll realize that it’s more about small iterations than big-bang changes. Hopefully this mindset will yield better results and peace of mind 😄
Closing remarks
Wow! That… was a lot of information, wasn’t it? To be honest, this is just the tip of the iceberg. We could practically turn each section into its own blog post, and we would still not do justice to it.
Because of the layered nature of React Native, it’d be prudent to study things on a need-basis. If you don’t need it immediately, you’re probably better off not knowing about it. You’ll better utilize your time diving deep into immediate problems. E.g. Fixing expensive re-rendering in React can yield better/faster results than looking for solutions in unknown native territory. In other words, you can explore the native land after making your javascript bundle as performant as possible.
React Native is very prone to crashes because of myriad reasons. Putting functionality behind feature flags will safeguard your users from unexpected errors.
And in the end, nobody can learn every single detail of the system. Every layer of React Native is a rabbit hole. It’s easy to get consumed. So be kind to yourself if you find an unknown territory, take your time, start small and enjoy the process.