What I learned from building my first React Native App
I recently released my first app on Android built with React Native. It's a fully fledged workout log for the PHUL fitness program including customizing workouts and exercises, a rest timer, a calendar, progress charts, etc. You can see the full feature list and check out the app PHUL Workout Log on the Google Play Store. I don't own a Mac so it's only available on Android for now.
This was my first app created with React Native and the first bigger project I used Redux with and in this post I 'll talk about what I would do different if I were to start all over again. If you only read the documentation of a framework, you cannot judge whether it 'll be actually good for production, as there will always be unforeseen obstacles, like missing components, no access to system timers, and other things you didn't think about. Therefore, I'll talk about my experiences now after having created a full app with React Native and show some of the obstacles I encountered along the way with their solutions.
#Redux and App Design
I used Redux to handle the app state as I like the idea of having a single location, the store, where all the state is stored. I had no issues with it and would use it again, it's a great way to handle state while being easy to maintain and to refactor. Just make sure to follow good redux design choices, like only accessing the state in components through selectors, putting all business logic into the selectors and reducers (and thunks) and having stateless services. If you're new to Redux, I recommend the video serious on egghead.io by Dan Abramov himself (and part two talking about more design patterns). Also, reading this article by Tal Kol helps a lot and summarizes all important choices. Still there are things I would do different in designing my app now:
#Use a normalized state tree
At first, I had a state tree that contained deeply nested objects, but it was annoying to change the state tree, because you cannot simply mutate the state. Instead you have to copy all the objects along the path to your deeply stored object. Then, I refactored it and am now using a state that is broad instead of deep and complies to the ideas of normalizr. I'm a fan of doing most things without additional libraries when learning the principles of a new technology for the first time to really understand what's going on, so I ended up writing my own implementation to mutate the state safely. Next time, I will just use an immutability library like seamless-immutable.
Working with a deeply-nested state tree in reducers can get annoying.
#Put everything into the state
When a lot of components are mounted and pushed onto the route stack, every single component that is connected to the store calls their selectors when a state update occurs, even the ones that are not being rendered right now. For example, in my app you can choose one of four workout days in a WorkoutPicker Component. If you select one, the
WorkoutPicker component gets pushed onto the route stack to be able to transition back to it nicely. The problem is that
WorkoutPicker is connected to the store and the selectors iterate over all workouts to compute the average workout duration, which is a costly operation to do on every state update.
Reselect helps in the way that it caches the result and does not recompute it if an irrelevant part of the state tree (for the computation) changed (for example a change in the settings). Again, I'm a fan of doing things myself the first time (and you always get a blog post out of it), so I implemented my own memoized selectors. Part of the reason why reselect didn't work well for me was because reselect's selectors only access the state tree and cannot take additional arguments. So make sure to really store everything in the state tree if you want to use reselect. (For instance, I only stored the active route in the state while leaving the route stack implicit in the Navigator.)
#React Native Experiences
In fact, I'm pretty sure if I got into native Android programing (I
have had no experience with it) and wrote the same blog post from that perspective, the list would be much longer.
#Components I wish already existed
Popup Menu: This one actually exists in react-native but is not documented, see this post here.
Push Notifications on Android: I really think this one should be in the React Native Core. There is already an API for iOS, but not for Android. I used local notifications for the rest timer in my app, as these work even when the app is not in the foreground or when the device is sleeping. There is a library react-native-system-notification which I forked, because some things were not yet implemented that were important to me: Working LED lights on notifications and custom vibrate patterns. The alarm timer also has a
bugfeature since Android.KITKAT that makes the scheduled notifications fire off later instead of exact in trade for better battery consumption.
Even though I had no prior Android experience, I was able to fix these things by simply looking up the documentation as it just requires Java knowledge but nothing Android specific. I don't like having unused app permissions or dependencies, so I ended up removing some things from react-native-system-notification that I didn't need: Unnecessary app permissions like
RECEIVE_BOOT_COMPLETED, and the dependency on sending web push notifications over GCM/FCM because I only use local notifications.
Background Timer: It would be nice to have a general background timer in the react-native core that calls a callback after some time and still runs when your app is not in the foreground (
setTimeoutand similar don't). I only needed this functionality for the notifications, so I didn't end up using any other library here.
Other highly requested features: You can see a lot more requested react-native features and bug fixes on productpains, if you want to get an overview of the current state of react-native.
#Bugs and Performance Issues
I didn't have any issues with performance, because I was always on the lookout for possible performance bottle necks and came up with optimizations as soon as I had concerns. Just make sure to remove all debug
console.logs when publishing. You can try out the app to see if it really feels native.
The only bug I encountered in react-native is that
ViewPagerAndroid doesn't work when it is contained within a ScrollView, so I had to rewrite the
If you don't get paid to build the app, you probably want to earn at least some money from it.
There is react-native-billing for Android, but I haven't personally tested it. It's probably hard to get an in-app purchase module working for both Android and Apple, but I'd love to see it in a future React-Native update.
I feel like in this area react-native, or rather the advertisement platforms, are lacking a lot and don't recognize the huge opportunity here. Right now, there is only react-native-admob which I use to show a small ad-banner at the bottom of the app. It shouldn't be too hard for other ad providers to release an official react-native module - I'm sure a lot of people would try it as there is just no competition right now.
The workflow in React Native is really nice, no doubt. The hot-reloading makes it incredibly easy to test changes and this amount of developer satisfaction is not even possible by building native Android apps. The only issue I had were the constant EPERM lstat File Access Errors I got when trying to compile with react-native run-android under Windows.
There could also be better error descriptions, a lot of times I felt like the error message had nothing to do with where the actual error is located in my code.
I had some memory leakage issues when testing my charts in a
ViewPagerAndroid while hot reloading the app in dev mode. The app just crashed due to a memory issue. This didn't happen in the release build yet, so to me it looks like some resources are not being released while hot-reloading?
java.lang.OutOfMemoryError: Failed to allocate a 6058404 byte allocation with 3950232 free bytes and 3MB until OOM at dalvik.system.VMRuntime.newNonMovableArray(Native Method) at android.graphics.Bitmap.nativeCreate(Native Method)