React Native at Tiki

Disclaimer: As of May 2018, Tiki already sunset React Native.

At Tiki, we have been developing new features for our Android and iOS app, in React Native. If you’re evaluating React Native for your next project, or just curious about it — here is our story. In this article, I will share our overall experience with RN, and the things we have learned so far.

Why React Native?

Hot reloading & Live reloading

We love the live reloading capabilities in React Native. In native languages, we have to wait until the completion of compilation up to ~120 seconds to see the final output. But React Native lets us modify the app without recompiling it. JavaScript code is loaded from the local server during development, and is packaged into the app with other resources when we release the app; which definitely speeded-up our UI iterations.

React Native also has a feature called Hot Reloading. It keeps the app running, and injects the new changes in realtime. At this time, hot reloading is not working perfectly in a few situations, but a reload of the whole app, usually fixes it.

Reusability

Another amazing feature of React Native is that the same generic code can be used on both the iOS, and Android platforms. For instance, if you use React Native’s switch component, it will render UISwitch in iOS devices and the Native Switch component in Android. In our app, we have reused around 80% of the JavaScript code between Android and iOS.

Incremental adopting React Native

We started using since early 2016 for experimental feature like picking attribute for a product.

Because converting completely to React Native comes at very high cost for us; plus some features requires native performance. We decided to become hybrid app.

Our business held an annual event, named “Dzut Co Hon”, on the web to express gratitude to our customers as usual. However, last July the event also took place on mobile. Brief description about the event on July in the lunar calendar, in many countries like Vietnam, China, HongKong, people give things away to pay homage to the death. At Tiki, we slashed our price up to 80% instead of giving. All users need to do is tap on the expected products. The quickest the winner.

Because of up to 60k concurrent users at a time, fixed schedule for events. We had no time for bugs fix, Appstore and PlayStore approval process. React Native came to the rescue.

Problems we faced

Frequent Release cycles

React Native is in active development. There are many features, and bug fixes in each release. But every release often brings breaking changes as well (maybe in a “move fast and break things” spirit specific to Facebook). In these cases, because we have a few features using React Native, we have to spend a lot of time fixing things, bundling while upgrading new version.

Slow page transition

After hitting the sign-in button, we move to the next screen right off the bat if the event was happening.

When we push a route to the navigator, JavaScript needs to render all the components which are necessary for the new scene, and it also needs to do the page transition in the same JS thread. Doing both the things simultaneously gave a choppy transition to us. So, we started scheduling the animation using InteractionManager.runAfterInteractions(). Before we made the transition, we pre-loaded all the tab data, and product lists on the Register Screen. Once the transition was complete, we just focused to render the content. It helped us provide a smoother experience to our users.

Tab Layout

We tried one of the most popular of these libraries is react-native-scrollable-tab-view. It’s a pure javascript implementation. The behavior comes very close to material design pattern and has extensive props to gain complete control. The library started showing performance issues because of complexity of our UI. Since it was a pure JS implementation, there were latencies communicating touch interactions across the bridge. We decided to write our own native RNTabLayout and ported it to Javascript realm.

Platform-specific code

When our QA team tested count-down functionality. A very strange bug was filed. iOS count downs faster than Android does. We tried to solve problem with an experiment.

let currentTime1
countDown(10000)
let currentTime2

We expected currentTime2 — currentTime1 = 10000 on both platforms. However, it’s just true on iOS. In addition, iOS rendered the product list as above much faster than low-end Android devices did. We had to handle with a little trickery.

getTimeRemaining(endTimeInMillis) {    
const platformDelta = Platform.OS == 'ios' ? 1000 : 1600;
let diffInMillis = new Date(endTimeInMillis).getTime() - Date.now() - platformDelta;
...
}

Performance issues

“Dzut Co Hon” was initially based on Redux. What we expected was to click on the product, it should have immediately shown a progress on it and instantly changed to selected state after network call. In reality, it took us a second even more when multiple products were tapped. After having investigated, we noticed that it took much time for Reducer to receive multiple dispatched actions from ProductComponent . We decided to use setState() inside ProductComponent and leave Redux out in ProductListComponent .

As for our new app home where RN 0.42 is used, ListView is still a huge issue for us because of our large, complex multiple view types and tabs.

New app home

It feels janky when scrolling. We will convert to FlatList soon to see how it goes.

As for multiple tabs with ViewPager, when all the tabs are loaded, all ListViews and the whole Store are kept in the memory . OutOfMemory occurs often on low-end devices. We have to retain data only for the active tab in Store . Data for other tabs is pushed and persisted on LocalStorage . Put it another way, other tabs have nothing to render but a loading view. . When users back to the previous selected tab. It only takes time to retrieve from disk cache and render.

We make great use of shouldComponentUpdate() and PureComponent. When users slide a banner, a deal…, the indicator is changed. Consequently, its parent and maybe the whole component tree must reconcile causing performance hurt. Implementing shouldComponentShouldUpdate() to make sure other components won’t be affected.

Continuous Integration

React Native feature resides in a separate repo

At Tiki we use Jenkins to deploy native apps to staging or production environment. While developing with React Native we want to have the same development process. When a commit is merged to the staging branch, tests are run and Jenkins pushes the latest JS bundle to CodePush server, then notifies us via Slack channel.

Based on a sample, we wrote a custom CLI tool to run code-push release-react "TikiHome-ios" ios -d "Staging" — des "Jenkins deploy" — dev true -m false -t . Once the dev branch is good to go, we merge the code into the master branch. Jenkins runs all tests, builds a production target, deploys the latest bundle, create a tag version.

Conclusion

We strongly believe that React Native is a fantastic framework. While it does have a few issues, those issues are overshadowed by the mountain of benefits you get from using it. We’re happy with the choices we’ve made — they’ve allowed us to move much more quickly on our mobile products.