Skip to main content

Experimenting with React Native at Ruangguru

· 7 min read
Agastya Darma Laksana
Staff Software Engineer

By Agastya Darma | Staff Software Engineer, Platform.

This is the story of why we chose React Native and how we overcame the technical challenges we encountered along the road.

BrainAcademy build with React Native running on our existing native app.

Early last year, a small group of developers at Ruangguru began studying the feasibility of implementing React Native. React Native allows you to create iOS and Android apps in JavaScript using the declarative programming language of React. This leads in more concise, easier-to-understand code, faster iteration without a build cycle, and easy code sharing between platforms. You can ship faster and focus on the things that count, making your app look and feel fantastic.

Since its initial release, React Native has come a long way. It is being used not only in Facebook and Instagram, but also in a wide range of other businesses, from Fortune 500 corporations to hot new startups.

The Reasons#

We began investigating the use of React Native in early 2022 to allow product teams to release features faster through code sharing and faster iteration speeds, using techniques like Live Reload and Hot Reloading that remove compile-install cycles. As a result, the product engineering team is now able to deploy the same functionality on iOS and Android with a single codebase, and perhaps in the future, we will be able to ship the same React Native codebase to the web. So with React Native the App feature will be written only once.

Ruangguru universal frontend. A teaser for our single codebase multiplatform solution.

The feature created with React Native will be injected into the app at the same time working side by side with the existing native code. The cross-platform feature is developed and maintained by a single team. So there’s no longer a large panel of native engineer experts involved.

The Challenges#

When you integrate React Native into an existing native project, you may experience obstacles and additional effort that you do not encounter when you start from scratch. With this in mind, we chose to begin investigating these challenges by porting the most appropriate part of Ruangguru apps: the Brain Academy module.

Slow Communications Between Native and React Native#

If you look at the React Native layout, you’ll notice that it uses a Bridge to connect with native modules, which often results in a queue because these two sides are unaware of each other. Such a process may result in performance constraints that have a detrimental impact on the user experience.

However, with the introduction of the React Native re-architecture, the Bridge will be gradually phased out and replaced by a new component known as the JavaScript Interface (JSI). Ruangguru will utilize JSI to facilitate communication between the Native and Javascirpt Realms to overcome this challenge.

Start Up Performance#

Previously, React Native had a significant startup overhead due to the need to inject the JavaScript bundle into JavaScriptCore (the VM used by React Native on both iOS and Android) and instantiate native modules and view managers. This problem is dramatically reduced in 2022 thanks to the Hermes engine. Based on our testing the initial loading for React Native is very minimal.

Android Bundle Size#

The first issue was adding React Native as a dependency into an existing App would significant impact the binary size. But this would not only increase the binary size, but would also have a significant impact on the number of methods count. We ended up pulling in only the React Native pieces we required at the time and building our own implementations for those that relied on libraries we didn’t want to use.

However, this solution still adds megabytes to the app binary size. As a result, we will include the React Native module with the Dynamic Features Module. Because we made React Native an on-demand module, users will only need to download the React Native module when they need to use the feature. This significantly reduces the app install size.

Utilizing The New React Native Architecture#

This section will discuss how Ruangguru uses JSI, a component of the new React Native architecture, to make navigation events, sharing data, and resource synchronization considerably faster than before.

The Old Architecture

The Javascript and Main threads do not communicate directly under the old architecture, but rely on the Bridge. The existence of a serializable bridge results in wasteful copying rather than memory sharing between threads. Because everything runs asynchronously, there is no guarantee that the data will arrive on time or at all. Larger data transfers may be extremely slow. Native module features that need synchronous data access cannot be fully utilized.

The New Architecture

The new architecture completely removed the Bridge. Javascript can now store references to C++ Host objects and call their methods. Time-sensitive tasks, such as user interaction with the app, can be carried out synchronously, eliminating the need for serialization and deserialization during runtime.

Next Generation Native Module That Ruangguru Uses.

All Native Module that Ruangguru uses is using JSI the backbone of React Native’s new architecture.

Initializing a JSI module.

The process of initializing a JSI module differs from that of an ordinary React Native Module. We have an install method here that will be called once from the JavaScript realm to initialize the “connection.”

The JavaScript realm must first check to see if the module is appropriately installed; if the module is not present, the JavaScript realm will invoke the install method.

jsiRuntime.global().setProperty method.

Because we’re utilizing JSI, we’ll utilize the install method to initialize the native function we want to share between the Native and JavaScript realms. As you can see in the code, we use the jsiRuntime.global().setProperty method to share the function between the two realms.

So finally, to use this on the JavaScript Realm, we can simply use the method like the picture above. So, in short, with the new architecture, we eliminate the need for the old react native bridge, eliminating the waiting time for the Asynchronous method to resolve, and enable instant communication between the Native and JavaScript realm.

Results#

React Native enabled product teams to release features to both our iOS and Android apps more quickly.

Startup Time#

React Native cold startup on Android.

After enabling Hermes and preloading on react native bundle, we can achieve desirable startup time when switching pages from Native to React Native. Users should not be able to distinguish the loading time between a Native and React Native screen.

Navigation#

BrainAcademy Navigation example.

Navigation between React Native to Native screen and vice versa is rapid and should not be noticeable to the users. As shown in the demo above, the navigation from BrainAcademy live teaching list (react-native) to the live teaching front page (native) is as fast as usual native navigation.

In the example above we demonstrated the capabilities of Ruangguru React Native navigation.

BrainAcademy Home (RN) -> Live Teaching Front Page (Native) -> Cek Ombak (RN).

Dynamic Feature Module (Android)#

Dynamic Feature Module for BrainAcademy React Native.

Because BrainAcademy React Native is an on-demand module, users will only need to download it when they need to use the feature. With DFM enabled, we drastically minimize the cost of React Native on android.

The Future#

Every decision always involves trade-offs, and we recognize that there are no silver bullets when selecting a technology, but we believe the benefits exceed the drawbacks. We are very optimistic about Ruangguru’s future with React Native.

With the expected productivity increases, we may begin to build product feature teams with cross-functional team members collaborating to address current and future business difficulties. Soon, we will also investigate how to leverage React Native javascript ecosystem into the greater Web ecosystem. Because React Native leverage and use the existing web ecosystem, we are thinking of shipping the same codebase to the web.

We think that by implementing React Native, we will be on the most efficient and effective route to developing the Ruangguru family of apps that satisfy the demands of our customers.

Footnote#

Please visit our careers page if this blog article piqued your interest in what we’re doing.

Agastya Darma, Sean Urian and Jevi Saputra are Software Engineers working on the Core React Native team at Ruangguru.

Recoil, A State Management Library For React

· 6 min read
Agastya Darma Laksana
Software Engineer

Biggest Challenge in React application is the management of global state. In large applications, React alone is not sufficient to handle the state complexity which is why some developers use React hooks, Redux and others state management libraries.

Do You Need A State Management Library?#

For reasons of compatibility and simplicity, it's best to use React's built-in state management capabilities rather than external global state like Recoil. But as i said before React has certain limitations when it comes to a global state management.

  • Component state can only be shared by pushing it up to the common ancestor, but this might include a huge tree that then needs to re-render.

  • Context can only store a single value, not an indefinite set of values each with its own consumers.

  • Both of these make it difficult to code-split the top of the tree (where the state has to live) from the leaves of the tree (where the state is used).

So When Should We Use A State Management Library Like Recoil?#

Applying a global state management is not so easy, it is a lot off hard work and it also takes time to implement. So, it is very important for you to know when to implement the state-management.

  1. If your application contains a large number of components and a lot of requests are being sent to the back-end for data retrieval, then it becomes mandatory to implement the state-management, as it will boost the user experience and speed of the application to a great extent. With a global state, you don't have to fetch the same request multiple times as the data will already be "cached" from the first request and can be consumed by other part of your screen.
  2. If you use redundant data throughout the whole app, for example, a list of customers is being used in the invoice creation and sales report generation then there is no need to fetch customers again and again from the database. You could simply just put the data in the global state.

What is it about Recoil.js that’s so appealing?#

Recoil feels just like React. The syntax is similar to React and it looks like a part of React API. Other than that, it has many other upsides like it solves the problem of global state management, shared state, derived data, etc. The team at Recoil make sure that the semantics and behavior of Recoil be as Reactish as possible.

The Recoil Concept.#

Recoil is an experimental state management library at Facebook, created by Dave McCabe. The reason why i like Recoil better than Redux is because Recoil solves all our complex state management problems but its configuration is surprisingly simple, unlike Redux. And we do not need to write much boilerplate code as we would have by using other state management library like Redux.

Installing Recoil#

As Recoil is a state management library for React, you need to make sure that you have React or React Native installed and running before getting started.

npm install recoil// oryarn add recoil

Core Concept of Recoil#

There are two core concepts of Recoil that you need to understand. This are Atoms and Selectors.

Atoms#

Atoms are units of state. They're updateable and subscribable: when an atom is updated, each subscribed component is re-rendered with the new value. They can be created at runtime, too. Atoms can be used in place of React local component state. If the same atom is used from multiple components, all those components share their state.

You can create Atoms with the atom function:

const countState = atom({  key: 'countState',  default: 1,});

Atoms use a unique key for debugging, persistence, and mapping of all atoms. You can't have a duplicate key among the atoms. So because of that you need to make sure they're globally unique. And also like a React component state, they also have a default value.

To read and write an atom from a component, we use a hook called useRecoilState. It's just like React's useState, but now the state can be shared between components:

function CountButton() {  const [countValue, setCountValue] = useRecoilState(countState);  return (    <>      <h4>Count Value {countValue}</h4>      <button onClick={() => setCountValue((value) => value + 1)}>        Click to Increase Count      </button>    </>  );}
Selectors#

A selector is basically a piece of derived state, where ‘derived state’ can be defined as the ‘the output of passing state to a pure function that modifies the given state in some way’. So in short when these upstream atoms or selectors are updated, the selector function will be re-evaluated. Components can subscribe to selectors just like atoms, and will then be re-rendered when the selectors change.

const countLabelOddEventState = selector({  key: 'countLabelOddEventState',  get: ({get}) => {    const count = get(countState);    if (count % 2 == 0) {      return `isEven`;    }    return `isOdd`;  },});

As you can see Selectors also have a unique ID like atoms but not a default value. A selector takes atoms or other selectors as input and when these inputs are updated, the selector function gets re-evaluated.

The get property is the function that is to be computed. It can access the value of atoms and other selectors using the get argument passed to it. Whenever it accesses another atom or selector, a dependency relationship is created such that updating the other atom or selector will cause this one to be recomputed.

Selectors can be read using useRecoilValue(), which takes an atom or selector as an argument and returns the corresponding value. We don't use the useRecoilState() as the countLabelOddEventState selector is not writeable (see the selector API reference for more information on writeable selectors).

Conclusion#

Personally i think Recoil is a great library but unless you have some specific problems regarding global state management, you don’t really need it. It's nothing that the developer’s world couldn’t survive without. You can even use Recoil partially in your application, exactly where you need, without having to adopt it for the entire application.

References#

Recoil Core Concepts.

Recoil.js — A New State Management Library for React.

React Native Code-Splitting With Repack

· 5 min read
Agastya Darma Laksana
Software Engineer

When you are developing a React Native application you will most likely be writing a lot of JavaScript code that contains dependencies that usually come from external repositories like NPM. The compilation of these many JavaScript files and dependencies will be processed into a single bundle file that can be read by React Native. In React Native this compilation will be done by default by Metro Bundler.

So basically Metro Bundler works by taking your source code, all external library dependencies as well as static assets that you use and converting them, optimizing them, and packaging them into a single bundle that can be run by React Native.

What is Code-Splitting?#

Code-Splitting is a technique that allows developers to create multiple bundle files from existing code sources, so the resulting bundle is not just one but consists of many files which are commonly referred to as chunck.

By default, all your input files (source code, dependencies, and assets) are combined into one file. With Code-Splitting, the bundle will be divided into several parts called chunck. The main chunck (also known as the entry chunk) is commonly referred to as the main bundle.

Why Do You Need Re.pack To Code-Splitting?#

As explained above, by default React Native will use Metro Bundler to do JavaScript bundling. But unfortunately Metro Bundler cannot perform Code-Splitting technique by default. To be able to perform the Code-Splitting technique, we need a JavaScript bundler other than Metro Bundler.

What is Re.pack?#

Re.pack is basically a toolkit that allows you to use Webpack and its Code-Splitting functionality and use them on React Native.

Why Do We Need Code-Splitting in React Native?#

Single bundles aren't a bad thing, but as your app grows your bundle size will grow as well. Especially if you use a lot of third-party libraries you need to be very careful about the code that will be included in the application bundle so that you don't accidentally make your bundle so large that it takes a long time for React Native to load your application.

Increasing App Startup Time#

If you have performance issues in the application startup area Code-Splitting is a technique worth trying when you have those problems.

Moving code from a single bundle to multiple chunks of chunck if properly configured can make your React Native app to only load the required code snippets and delay loading of other code on startup. This really helps improve the startup performance of your application.

Modular Applications#

Apps that expose different functionality or different UI based on user details are examples of apps that would benefit greatly from Code-Splitting.

Let's take an example of an e-learning application like Ruangguru. With Code-Splitting, you will be able to separate student and teacher functionality in separate bundle files, so that the application only loads one of the bundles, this can improve startup performance by loading only code that is relevant to the user's needs.

Two other groups of apps where Code-Splitting plays a big role are super apps (like Gojek) as well as apps with mini app stores (like WeChat/Line). Instead of having multiple apps on the App Store and Google Play, you can combine them into one while streamlining development and simplifying management.

How to Use Re.pack for Code-Splitting in React Native#

How to use Re.pack? The first step to using Re.pack is to install the required dependencies:

npm i -D webpack terser-webpack-plugin babel-loader @callstack/repack# oryarn add -D webpack terser-webpack-plugin babel-loader @callstack/repack

After that, Create react-native.config.js (if this file is not in your project) and copy the content below:

module.exports = {  commands: require('@callstack/repack/commands'),};

Then configure XCode and Gradle to use the webpack-bundle command. By making this change you will replace the default Metro Bundler to Re.pack

  • XCode: Add export BUNDLE_COMMAND=webpack-bundle to the Bundle React Native code and images phase of Build Phases in your XCode project configuration.

    export NODE_BINARY=nodeexport BUNDLE_COMMAND=webpack-bundle../node_modules/react-native/scripts/react-native-xcode.sh
  • Gradle: Add bundleCommand: "webpack-bundle" into project.ext.react in android/app/build.gradle file, so it looks similar to:

    project.ext.react = [    enableHermes: false,  // clean and rebuild if changing    bundleCommand: "webpack-bundle",    bundleInDebug: false]

All configurations have been completed. Now you can use repack in your React Native application.

To run the development server you can use the command

npx react-native webpack-start

What are the disadvantages of Repack compared to Facebook Metro Bundler ?#

Of course there are some drawbacks when we use Code-Splitting with Re.pack instead of Metro Bundler. One of the biggest drawbacks is that we can't use Codepush anymore to do Hot Push code in Production Env.

Besides that at the time this article was written if you use Hermes with Re.pack then it can only convert Main Bundle to Hermes Bytecode, chunck files outside of Main Bundle will not be transformed into Hermes Bytecode.