Being one of the first apps built with React Native, we were excited to share our first year journey using React Native back in 2016.
Looking back at the past three years, React Native has proven to be extremely successful at Discord and helped drive our iOS user adoption from zero to millions!
More specifically, React Native has allowed us to reap the benefits of quickly leveraging reusable code across platforms, as well as develop a small and mighty team.
Meanwhile, we’ve learned to adapt to its inevitable pain points without sacrificing overall productivity.
Start with iOS, Try Android Later
We tried React Native literally the day it came out, holding off on building our iOS app because we knew it would be announced at React.js Conf 2015.
Similar to iOS, we tried React Native the day it was released for Android. We were surprised by how easily and quickly we were able to make our comprehensive iOS app run on Android — took only two days and it built!
However, we immediately stopped after identifying various issues such as poor performance of touch events and lack of 64-bit support. We continued to follow React Native Android through observing many other apps and blog posts, but there have been no fundamental improvements to convince us to try again.
Though React Native was not ideal for Android, we realized it would be for iOS. While it might seem odd to choose React Native for only one mobile platform, especially considering React Native was all about “learn once for mobile instead of twice”, it’s given us a lot of benefits, such as:
- React and Flux make up our web front-end tech stack. Therefore we can easily leverage over nearly all of the business logic (i.e. stores and libraries) and a majority of our front-end tooling and infrastructure (e.g. eslint, prettier and flow) from web to iOS.
- Allowing our React Native engineers to focus on a top-notch iOS app without needing to be proficient at the Android platform.
Small & Mighty Team
The Discord iOS app has millions of daily active users and 4.8 stars with over 240k ratings. This has all been accomplished with a team of two engineers!
React Native allows us to keep our team small and efficient while maintaining Discord values like:
- Offering great opportunities to learn and grow. I joined Discord as a native iOS developer, and conversely, our other developer Victoria joined with a web development background. Like many other engineers at Discord, we both were motivated to step out of our comfort zones, learn a new platform, and eventually become more versatile engineers.
- Sharing as much code as possible without sacrificing the user experience. Front-end engineers really own their features across web and iOS platforms through reusable code.
Just like our iOS team, Discord is thriving through learning, growing and leveraging expertise every day. As a result, we are incredibly proud of our amazing engineer to user ratio at Discord — 40 engineers to 130M+ users.
Moving Fast
Faster Iteration of UI components
Without diving into the app, we can use storybook to quickly render "dumb" components that don't manage any state internally with mock props. By leveraging the same tooling, we can quickly follow the web team who originally adopted storybook. Along with hot reloading, storybook dramatically accelerates our UI development cycle. Our designers can even easily tweak styling directly in storybook.
Easier React Native Upgrades
It was painful and time consuming to upgrade our React Native fork which fixes issues specific to our use cases. Nowadays, releases are more stable. Compared to spending days before, it only took us few hours to upgrade our fork to 0.55 from 0.53 recently.
However, our web team usually upgrades React and/or other shared dependencies faster than the iOS team. Being on different versions of shared frameworks may cause non-obvious problems which leads to time-consuming investigation. For instance, the iOS app encountered an instant crash in a full-release build although it ran fine locally. After bisecting commits on master, it turned out that the web-only webpack 4 upgrade broke the iOS build due to shared base configs. However, we were able to learn from those issues and apply the knowledge to our infrastructure which ultimately benefits all the teams.
Over The Air Patches (OTA)
A few days ago, we were able to quickly deploy a post-release fix for cameras and skip the approval process. We can’t even begin to count how many OTAs we’ve shipped. Being allowed more room for mistakes, engineers can ship often with confidence and avoid coordination, which unblocks everyone.
And More…
In the past three years, React Native has proven to be a great platform which allows engineers to move at an unparalleled speed. Getting it right has required a fair bit of learning, so we’d like to dive deeper into how we’ve learned and adapted to some pain points without sacrificing overall productivity.
Learn to Adapt to Pain Points
Like any technology, React Native is not perfect, but it excites us to see Facebook and the community at large actively working towards improving or eliminating its weaknesses like large-scale re-architecture and better flow type coverage.
React Native is certainly a revolutionary and fast-moving framework, so the challenges we face today are quite different than what we faced three years ago. For instance, while learning to reuse front-end tooling, we started with React Native Webpack Server, but after a year found out it was abandoned. We eventually had a non-trivial migration to Haul for React 16.
Next, we want to share how we conquered our top five pain points with React Native. Hopefully, it will help you too!
1. Immature Long Lists
Immature long lists have been a well known issue since day one. Without a doubt, we have encountered the same problem in our core component: the chat view. Alternatively, we use UITableView for smooth scrolling performance and dynamic cell heights. Furthermore, our chat view is extensively controlled by Javascript. It computes all the data sources in Javascript, passes them to Objective-C as properties, renders the entire table view in Objective-C via Yoga directly (the same layout engine React Native uses), and finally sends events back to Javascript for user interactions.
It’s worth mentioning that this chat view is so far the only native view we have, which means the hundreds of other of React Native components we have are able to provide a satisfying 60fps performance.
2. High-Priority Updates
Facebook is solving this problem by re-architecting the threading model:
It will be possible to call synchronously into JavaScript on any thread for high-priority updates.
We also found a quick workaround for high-priority UI updates while we worked on synchronous keyboard layout animation. All the keyboard layout solutions are one frame behind when start dismissing keyboard and therefore results in a noticeable gap between TextInput and keyboard.
After various attempts at solutions like InputAccessoryView, we eventually figured out a way to "portal" the React Native component into native iOS views hierarchy and therefore manage high-priority keyboard updates synchronously in native. We will detail this more in a later blog post.
3. Requires Native Platform Knowledge
In React Native world, a web developer will take much longer to succeed than a native developer. Most of the time, it has been smooth sailing working on React Native. However, you sometimes come across very specific native technical details, or find yourself working on OS-specific features like the share extension on iOS.
Web developers will likely take longer to learn and sometimes need help from engineers with native experience. But once they learn and grow, they can share valuable information across front-end platforms and help keep our teams small and mighty.
4. Startup Delay
Time to Interaction (TTI) is a key aspect of how a user perceives the performance of an app. The TTI of our iOS app is noticeably longer than other competing messaging apps written in native code. Since React Native loads and parses all the Javascript code before execution, you get startup delay based on bundle size. In our case, it’s roughly 1.5s startup delay to load a 15mb bundle on an iPhone X.
Fortunately, Facebook documented unbundling to only load necessary portions of the bundle for startup and progressively load more as needed via their packager. However, Haul, our React Native packager replacement for webpack, doesn’t support unbundling/code splitting yet. We wish React Native was more webpack friendly out of the box; we will inevitably need our front-end infrastructure engineers to build this in-house.
5. Limited Visibility into Facebook
In the React Native repo, we sometimes see hot issues with limited activity in response from Facebook. Lack of visibility into such a large company can bring about frustrations when so many people depend on their open source contributions.
On the other hand, it forces us to dive into the core codebase in order to figure out the problem, which admittedly one of the best ways to learn an open source platform. By understanding the React Native platform deeply, we were able to maintain a fork that fixes issues for our own use cases and reuse their core modules. For instance, our UIImageView, for avatars and/or emojis in native chat, reuses RCTImageLoader for consistent caching.
Sticking with React Native
React Native is a great framework that bridges the gap between web and mobile. At Discord it has brought us incredibly efficiency. It allows us to write reusable code, learn from each other, and move fast with a two person team.
Although there are real pain points and challenges, the overall gains significantly outweigh the costs which motivates us to keep investing in the platform.
We’re always looking for the next great addition to our engineering teams at Discord. If what’s described here sounds interesting to you, and especially if you are a gamer at heart, check out our available positions here.