featured

Background

In 2018, the TenX product and engineering teams decided to migrate their existing native mobile apps to React Native. You can read the full motivation behind this decision in the earlier posts from our Product team here and here.

Starting in the summer, the engineering team began rebuilding the mobile apps from scratch using React Native. We were a small team driven by a pressing business need to ship a replacement for our native apps before the upcoming relaunch of the TenX Card.

By early December, we had shipped a new version of the TenX Wallet to our users on iOS and Android, fully rewritten in React Native.

Now, in 2019, the new TenX Wallet has supported the official relaunch of our cards in Singapore and is fully ready to support our card rollout throughout Asia and Europe.

In this post, I’ll discuss the migration to React Native from the engineering side.

App Architecture

From a high level, we built the app using Expo, TypeScript, and Apollo/GraphQL. These were important choices we made by looking at current trends in the JavaScript/React ecosystems and the benefits we would gain from each of these technologies.

GraphQL

One of the key decisions we made from the start is to transition our APIs to use GraphQL. However, at the time our native apps were communicating with our backend using a traditional REST API, so this meant we had to build a GraphQL server in parallel as we developed the React Native app. This would require additional work, but there were two key factors which made this tradeoff worthwhile:

  1. GraphQL would allow us to move very quickly on the frontend by giving us a flexible, typed API which we can manage with a client like Apollo.
  2. Our backend system was simultaneously being migrated to micro services, so this new GraphQL server could function as an API gateway which would free up the backend to start evolving independently from the frontend clients.

The decision to use GraphQL provided many key benefits to the React Native project. Because GraphQL provides a strongly typed API schema, we were able to build tooling to generate TypeScript type definitions and client GraphQL code, which we use directly in React Native. This means any API usage in the app is guaranteed to be correct (assuming the GraphQL schema is typed correctly!) and using the client queries or mutations is very easy because they are all auto-generated directly from the schema.

Furthermore, by using a GraphQL client like Apollo, our application no longer has to tediously manage loading and error states for network requests or handle storing response data in a global store — all of this is encapsulated by Apollo. As a result, we had no need to incorporate any state management library like Redux, any data fetching middleware like Redux Saga or RxJS, or, importantly, all of the associated code these patterns bring with them.

For some specific cases, we were able to use React Context to share state between multiple components (this works quite well, although we also used apollo-link-state effectively for the same purpose).

This approach eliminated the need for a surprising amount of code, which otherwise would have required a lot of time to write and probably caused various runtime bugs (state management and async IO is always error prone). Instead, relying on Apollo kept our codebase lean and allowed us to iterate very quickly and focus on the key business logic important to our users.

TypeScript

Our use of TypeScript was motivated by the many benefits it brings to the table: a static type system, object-oriented capabilities, and great tooling and ecosystem. Between TypeScript and our typed API schema courtesy of GraphQL, we had very thorough type coverage for the entire codebase.

Of course, 100% type coverage is hard to achieve for any application in any language. Misleading type information or the dreadful any can always creep in from the edges of your application (library dependencies, network requests, user input, etc.) and not all NPM libraries have available or accurate type definitions. Nevertheless, TypeScript definitely made our codebase easier to write, maintain, refactor, and debug.

Based on this foundation with TypeScript and GraphQL, many of our React components look something like the following code snippet, where the GraphQL helpers withUser and withUpdateUserInfoMutation and their corresponding types represented in theComponentProp interface are all auto-generated from our GraphQL schema.

import React from "react";
import { compose } from "react-apollo";
import { Button, TextInput } from "react-native";

import { Screen } from "src/ui";
import { UserQueryResponse } from "src/graphql/responses";
import { Success, UpdateUserInfoMutationArgs } from "src/graphql/types";
import { withUpdateUserInfoMutation, withUser } from "src/graphql/helpers";

interface ComponentProps {
user: UserQueryResponse;
updateUserInfo: IMutation<UpdateUserInfoMutationArgs, Promise<Success>>;
}

interface ComponentState {
email: string;
}

export class EmailScreenComponent extends React.Component<
ComponentProps,
ComponentState
> {
constructor(props: ComponentProps) {
super(props);

this.state = {
email: this.props.user.user.email,
};
}

render(): JSX.Element {
return (
<Screen>
<TextInput
value={this.state.email}
onChangeText={email => this.setState({ email })}
/>
<Button
title="Save Email"
onPress={() =>
this.props.updateUserInfo({
variables: { email: this.state.email },
})
}
/>
</Screen>
);
}
}

export default compose(
withUser,
withUpdateUserInfoMutation,
)(EmailScreenComponent);

This approach made it very easy to compose different behaviour with our components and maintained a very nice separation between the asynchronous network layer, the global store of application data, and the logic involved in rendering UI and handling user input. The rest of our application was organised into navigation stacks (we used React Navigation), screen components (like above), smaller, reusable UI components, and utility files full of common helper functions.

Testing

This leads into our approach to testing the application. Because this was a full rewrite which would replace an existing application with a pre-existing user base, it was important to avoid any serious bugs or regressions. Therefore, developing a very thorough approach to testing was critical in order to successfully launch the app.

In general, we followed the approach known as the testing pyramid, or one alternative I like a lot called the “Testing Trophy”. This approach emphasises static and unit tests first, with smaller layers of integration and end to end tests on top of this.

To start with, the first level of testing was our use of a strongly typed API and TypeScript, as described above, in addition to other automated tools to perform static code checks and enforce code quality like TSLint, Prettier, and even Danger JS. This helped to protect us against a lot of trivial bugs and maintain consistency throughout the codebase. All of these tools are automated in CI and run against builds and pull requests, which allow developers to focus on writing and shipping code.

On top of that, we built up an extensive suite of unit tests and maintained a strong discipline within the team to refactor any isolated logic into reusable functions which can be tested. As a result, almost every function in our codebase is unit tested, except for React class methods which are involved directly in rendering UI. This allows us to isolate and fix bugs quickly, and validate our changes with new test cases.

Diligent code review helps us maintain these standards, but using automation is also important. For instance, we use Danger JS to scan specific files in pull request diffs and prompt authors to add unit tests for any new functions they have added to certain files if no accompanying tests exist (or thank them if they already added the test!).

UI Regression Testing

From here, we also developed a tool which would render each screen in the app and record screenshots for it and then render these side by side for iOS and Android in a browser UI (see example screenshot below). This allows us to perform UI regression testing in a very fast way and allows us to review certain screens (e.g. screens that are hard to navigate to) very quickly.

It took some work to build this tool and automate the processes around it, but the tradeoff was definitely worth the small cost. Automating common processes or building simple tools to increase productivity almost allows pays dividends down the line.

Automated UI Testing

Finally, our QA team built a suite of automated UI tests in Ruby using Appiumand UI Automator which drive the app and walk through all major user flows, such as sign up, login, logout, change user settings, add wallets, set currency and wallet, and send and receive cryptocurrency. This proved very valuable as we continued to make many changes impacting these flows up until the initial app launch.

The final layer to our testing methodology was of course manual testing. We performed thorough manual testing with our QA, Product, and Design teams before testing the app in a beta round internally before launch. The disciplined approach to building testing into every level of our development workflow helped to give us a solid foundation for the initial launch of the app, which proceeded smoothly without any major issues.

Expo

The other key technology we used is Expo. For those not familiar, Expo is a development toolchain built around React Native. It allows you to develop and publish a React Native project with remarkable ease and speed.

Relying on Expo was one of the key decisions which allowed us to move as quickly as we did. This can be challenging, because Expo limits you to only those APIs they provide (you cannot link native code directly), but for the productivity tradeoff this can be worth it. In our case, we have so far managed to avoid ejecting from the core Expo SDK which has helped us maintain a fast iteration speed as we move forward.


The Good and The Bad

There are many tradeoffs involved in engineering decisions, and this is especially true with React Native. Here, I’d like to summarise some of the biggest pros and cons we experienced in rebuilding the TenX Wallet in React Native:

Pros:

  • All application logic is unified in one codebase. This is a huge win!
  • Over the air updates allow you to fix bugs in minutes, not days.
  • Full advantage of tooling and technologies in the JavaScript ecosystem, e.g. React, TypeScript, Apollo, GraphQL, Jest, etc.
  • React is a well known technology which makes it easy to onboard new team members or collaborate with other teams.
  • Tools like Expo and Expo Client provide an amazing developer experience and allow for very fast iteration speed.

Cons:

  • React Native and many other libraries are still young and rapidly evolving. You have to deal with changing APIs and be ready for frequent upgrades and breaking changes.
  • Documented APIs can fail unexpectedly, or may not be documented correctly (or at all). A good example is this open issue for React Nativeregarding the fetch API, which required a workaround using the XMLHttpRequest networking API directly. Issues like this can be very hard to diagnose and debug.
  • Users have high standards on mobile, and will inevitably compare your app to other mobile experiences they are familiar with. Sometimes it can be hard to live up to their expectations using React Native (for instance if screen transitions lag slightly, or long lists don’t scroll very well).
  • Some issues or questions are hard to find answers or support for online, simply because the community is still young and small.

The TenX Wallet Now

Since the initial launch of React Native, we have shipped several updates to our app and our teams have become more coordinated around this common codebase. With a unified codebase and the declarative nature of React, our design and product teams now regularly commit text or style changes directly to our codebase.

Issues would previously be addressed in a process like this: A designer finds a problem > designer files an issue > the developer team triages and fixes the issue > the issue is assigned to design or QA for testing > the issue is closed (or reopened if the developers didn’t get it exactly right!).

Now, our designers can directly fix issues they find and commit code directly for simple changes, reducing this process to a few steps and eliminating a lot of communication and process overheard, while also improving team cohesion and communication.

We have also recently onboarded a new engineer to the project, and this engineer was able to ship production code in their first week. The familiar foundation of React/React Native and the use of common design patterns made this process easy, and will prove beneficial in the future as the team continues to expand. We are also seeing other cross-platform benefits right now as we currently build a separate business functionality for web and are reusing code directly from our React Native codebase.


Takeaways

I think the biggest thing to keep in mind when considering React Native right now is to remember that native mobile technologies have had roughly 10 years or so to develop and mature.

React Native, on the other hand, is still quite young and is trying to solve a harder problem by building a cross-platform framework. It’s optimising for both platforms, while native app technologies are optimising for only a single platform.

Despite this, React Native is already fairly mature and demonstrates a lot of promise for the future. After all, one of the fundamental rules in programming is to not repeat yourself. React Native allows you to consolidate two duplicated applications into one clear and concise codebase, and this is very powerful for product teams who need to rapidly iterate on a company vision.

Nevertheless, like any new technology, React Native has its many quirks, idiosyncrasies, and pitfalls which can make life very challenging at times. Still, despite this, our decision to embrace React Native at TenX was definitely worth it. We were able to move forward from two legacy native iOS and Android codebases which were hard to maintain to a single application which our entire Product team can efficiently concentrate on. And, we are looking forward to the many new exciting improvements to this technology in the coming years (like React Native Fabric and other upcoming exciting developments in React)!

Download the TenX Wallet today! TenX Cards can now be ordered by residents of Singapore, Australia and New Zealand, and will be rolling out to the rest of Asia and Europe later this year.


If you have any further questions about TenX please…

Thanks to Andric Tham.