React Native at Tableau 3 Years
Three years ago the Tableau Mobile team decided to rewrite our mobile app in React Native. A bit more than a year ago we released the new app on iOS and Android. It has been a great experience. Using React Native has enabled us to ship more features, more often, with better quality. There have been challenges of course, but on balance it was the right decision for us.
In this blog post I want to share why we decided to use React Native, and what went well and what didn’t. Hopefully this will be useful to other teams out there considering React Native.
For a little context on the app, Tableau Mobile is a companion app to Tableau Online (Tableau’s cloud offering) and Tableau Server (Tableau’s on-prem offering) that lets you view your organization’s visualizations, dashboards, and metrics on your phone or tablet.
Where we were
Back in early 2017 the mobile team had to make a decision: rewrite our app or undertake some pretty significant refactoring. We had shipped both iOS and Android apps and our users were generally happy, but developers on the team were unhappy working in the codebase and our feature development was painfully slow.
The core problem was how our architecture had developed. When we first started working on the app we adopted Xamarin. This was mainly due to it being the most promising cross-platform solution. React Native didn’t exist yet, and solutions like Cordova were too limited. Code-sharing was very important because we didn’t feel like we were a large enough team to successfully maintain two separate codebases.
Xamarin Forms wasn’t viable at that time, so we decided to implement the UI separately for both platforms and share all non-UI logic. Fatefully though, we decided to ship on iOS first and hold off on working on Android. That seemed like a reasonable choice at the time, but after we released the iOS app we went to do Android and found that a lot of the non-UI logic we hoped to share was much too heavily tied to iOS paradigms. While building the Android app we were constantly refactoring the supposedly sharable non-UI code to get things to work, and eventually we gave up on sharing large pieces.
By the time we released the Android app we had ended up with a codebase that had a lot of platform-specific code, some of which was very awkwardly structured in layers below the UI. The codebase was very time consuming to work in and we were not even really leveraging Xamarin’s cross-platform capabilities. You could have argued that writing two separate apps from the start would have been better.
Eventually we collectively felt like we couldn’t continue in this state and so we had to decide between rewriting the app, or doing some significant refactoring.
Why did we rewrite in React Native?
Generally when choosing between rewriting and refactoring the safe choice is refactoring because of all the well-known dangers of rewriting a codebase.
However, that is a general rule and there are exceptions. We decided to rewrite because our problems ran so deep that we really did need to fundamentally rearchitect the app. Rewriting was the cheaper and more realistic way to do that.
Once we had decided to rewrite, that opened the door to choosing a different tech stack to build on. Xamarin had its fans on the team, but also its detractors. We debated rewriting with Xamarin, giving up on the cross-platform dream and going full native, and trying out this new kinda wild React Native thing people were talking about.
In the end we chose React Native for the following reasons.
First and foremost we were drawn to the promise of much faster iteration and higher productivity. We heard from other companies that things just went faster with React Native, and an initial React Native proof of concept app we wrote bore this out.
This was still very important to us. React Native promised to really let us be cross-platform with one codebase and a high level of code-sharing.
Sharing Technologies with Web
Being able to use the same technology and libraries that the web development teams at Tableau use, such as React, Redux and TypeScript, was a huge plus. Aside from the technologies themselves being great, it makes it easier for someone from the web world to come work on mobile, and lets us share knowledge and experience across both domains. Additionally the possibility of sharing code between mobile and the web was very appealing.
Community and Ecosystem
Even in 2017 the React Native community was large and thriving, tons of libraries were being created, and people were excited about it. This was really important; it gave us confidence that React Native was not going to just disappear and that we could find help when we needed it.
Ease of Integrating with Native code
We knew pretty much from the start that we would have to have some native code for certain features, so the ability to easily drop down to native when it was needed was a must have.
So we chose React Native and spent around a year and a half rewriting our app. We released the new app to the Apple App Store and Google Play Store in February 2019. Since then we have shipped new versions regularly, enjoyed a low crash rate, and have had lots of positive feedback from our users. Our usage has also grown by around 60%.
What went well?
Our team velocity is much higher now, and people are much happier working in the new codebase. One big reason for that is admittedly our new architecture, but it is also because of the great framework, tools, libraries, and especially the quick feedback cycle you get with React Native.
Maintaining feature parity between iOS and Android has been easy. Roughly 90% of our code, excluding authentication, is in shared TypeScript.
The reason for the qualification there about authentication is that we split out most of our authentication code into two separate native modules, one for each platform. Our app has to authenticate not only to our Tableau cloud-hosted servers, but also to servers hosted by our customers. There are lots of complicated scenarios we have to handle due to the variety of different authentication mechanisms that customers can use (SAML, NTLM/SSPI/Kerberos, OpenID, etc), and the fact that the servers can be behind corporate firewalls and reverse proxies. Very early on in the rewrite we realized that dealing with these authentication scenarios would require a lot of native code, very little of which would be related to the UI. So it made sense to split that code out.
Sharing Technologies with Web
There has been a lot more knowledge sharing thanks to using shared libraries and technologies. For example, when designing our architecture we leveraged the experience other teams at Tableau had had with libraries like redux-thunk and redux-saga.
We haven’t ended up sharing that much code between web and mobile, but we are making some headway there.
Community and Ecosystem
People were and still are excited about building things with React Native and the community is going strong.
There are many amazing community projects. We have saved developer years, maybe decades, because of these projects. Often you can just think of something you need, search for it, and find that someone has already implemented it and open-sourced it. The community is great and pretty responsive.
We have also open sourced a small library for DNS lookups, contributed back a bunch of PRs to various projects, and have aspirations to do more in the future.
When we started we were a bit worried about the possibility of Facebook losing interest in the project, but that hasn’t been the case. They have invested a lot in maturing and updating the framework and moving it forward, and they are more transparent now than when we started.
That said, on occasion we have taken a dependency on a project that appeared active only to have the maintainer switch jobs or lose interest and let the project stagnate. This has led to some fretting and at least one time-consuming migration.
As a brief aside, one great thing we found is that when there are bugs in our open source dependencies, we can just use patch-package to fix them locally. This way we aren’t blocked waiting on issues to get fixed or pull requests to get accepted, and we don’t have to resort to forking. This was not an option for third-party libraries consumed via NuGet with Xamarin.
Ease of Integrating with Native Code
Declarative UI w/ React
The React way of writing declarative UI has proven to be really effective for everyone on the team. Our UX designer can even work directly in the code sometimes to get the UI exactly the way they want. This is quite a bit different from the traditional back-and-forth workflow we had before, where the designer would create redlines, the developer would attempt to implement them, then the designer would update the redlines, the developer would update the code, etc etc.
When we started the rewrite we made Redux a core part of our architecture, and it has proven to be a great choice. It’s a somewhat unfamiliar architecture pattern at first, but once it clicks it just makes everything easier to understand. Having all state management and business logic in a consistent pattern really simplifies things. It also makes development easier in ways you might not expect, like being able to easily log any state changes in the app, or even log the entire state of the app.
VS Code is a great IDE and won over basically everyone who came to it from Xcode, Android Studio, or Xamarin Studio. We are also particularly big fans of Reactotron, which gives you a few methods to inspect and manipulate your app when it is running without having to activate the debugger.
With React Native there are some great tools and libraries that make it easy to add automated tests. Jest takes a lot of the pain out of writing unit tests. We have unit tests covering just about all of our TypeScript code. We also use both enzyme and react-native-testing-library in different cases for testing our React component rendering and behavior.
For on-device integration testing we have started to use Cavy, and so far it has filled the gap between our Jest tests and E2E tests nicely. We do still have a small suite of Appium E2E tests, but we rarely add new ones due to the cost to write and maintain them.
What were the biggest Challenges?
Developers new to mobile will also have to learn about native iOS and Android development, as React Native doesn’t completely shield you from it in our experience.
Living Further Away from Native
Using new flashy platform-specific stuff from Apple or Google can require finding third-party libraries or writing a React Native wrapper for it yourself. You don’t always get them for free, at least not quickly.
Additionally, cross-platform UI development requires some level of genericism, and, to be honest, compromise at times.
This is something worth accepting rather than fighting. Use third-party libraries where you can and try to keep things less complex. When you want the latest and greatest platform-specific feature allow some time for the community to support it, or support the community by filing issues and submitting PRs. Doing these things will let you share a lot more code and you will be leaning on React Native’s strengths.
There is no first-party navigation functionality built into React Native, and the closest thing is react-navigation. Which is a great library and we make heavy use of it, but it has struggled at times to seamlessly accommodate new things like the iPhone X notch and display cutouts on Android. We have invested a lot of time fixing issues here. It does seem like things have improved over time, but navigation is so fundamental to building a mobile app of any complexity that as a framework React Native feels incomplete without support for it.
Getting modals to look great on various form factors (both phones and tablets), resize appropriately (due to orientation changes as well as split screen), and accommodate things like the notch correctly has also been a big pain.
Dealing with upgrading React Native varies, in terms of enjoyability, between “boring chore” and “getting a cavity filled sans anesthesia”.
Overall it has gotten better since we adopted React Native, but the upgrades have rarely gone completely smoothly. We usually end up investing about 2 weeks of developer time on doing the upgrade and then trying to fix whatever broke. We try to upgrade every 3 months so we don’t lag too far behind, since the longer you wait the harder it tends to be. Using React Native Upgrade Helper has been a huge timesaver so take advantage of that.
Additionally, we have a large number of dependencies apart from React Native itself. We try to update them every 3 months, but offset from the React Native upgrades by 6 weeks. These upgrades are more consistently at the “boring chore” level of enjoyability, but the sheer number of dependencies still makes the task pretty time-consuming.
Choosing a new framework on its own won’t build a great app. A lot of the success we had rewriting our app was admittedly due to rearchitecting, but moving to React Native was also a huge factor. It was a very good choice for the Tableau mobile team, and we would be struggling to get to where we are today without it. While every team and project is different, we would definitely recommend it.
May your journey be as fruitful as ours.