Unifying mobile and web development with React Native

JOB TODAY is the world’s fastest growing mobile hiring marketplace operating in the US and Europe. Millions of candidates trust JOB TODAY to find local jobs in restaurants, hotels, shops and more. Over 100 million candidate applications were delivered since launch in 2015. Our mission is to help everyone find work in 24 hours.

In April 2015, JOB TODAY was launched on iOS, followed by Android and only this year, in May, we released the first web version of our service. Initially, our mobile applications were 100% native (Swift and Java). The team consisted of 4 mobile developers (1 Android, 2 iOS and 1 cross-platform) and 3 web developers.

As we continued to grow as a company, we decided to move to codebase unification and we selected React Native (RN). Our main reasons for moving to RN were quite common: we want to optimize and reuse our development resources which is crucial for a fast-growing startup. Gradually we migrated a significant amount of components to React Native.


Coincidentally, whilst we’re actively rewriting our app to React Native, a huge wave of negative posts about this framework have been published. We carefully read many of them and calmly continued our development :)

We needed a web app that would provide the same functionality for our users as native apps already do. We decided to not stop here in unifying our codebase: we looked at React Native for Web which was running Mobile Twitter PWA and with which we were able to use our React Native components in web. We decided to write simple web app reusing native components with React Native for Web first and then review and refactor modules and components to make them extendable and work properly in all our web and native apps.

After the first web version was released we did a code freeze for all apps and started refactoring. I’d like to share several of the architectural problems we came across and how we solved them.

Synchronous development for web and native

One of our main problems was the process of synchronous development for web and native, which was inconvenient and not optimised. Although web development was usual, with webpack server that provided fresh bundle after every source change. This was not the same case for iOS and Android apps. These apps were brownfield and over time we started to use React Native components as NPM private packages, however, development was carried out in sandbox (Storybook). In simpler terms, it was impossible to make changes in source code and immediately see them in native apps.

iOS and Android apps still have a native skeleton and we needed to combine this skeleton with React Native views. To initialise screens, navigate to them, handle and delegate events between native and JS sides via bridge. So we came to the concept of manager (internally we called it shell) which can take a JS bundle provided by metro and do all the functions listed above.

At the same time manager is connected to apps as pod library in case of iOS and as module for Android.

Manager is configured with a special app-structure.json file that tells the app where to get the bundle, what global props it has and which screens are contained. This configuration gives us flexibility, for instance by duplicating managers so that we have different bundles and several apps in one (for different user roles for instance) or do A/B tests.

Another function of manager is to navigate between screens. We use our own native implementation of navigator which can push, pop and present scenes. To make this work, we register each screen independently via AppRegistry with a unique key. The same key is set in the structure json file.

Each screen is also enhanced with withNativeBridge and withNavigation HOCs which wraps components with contexts to allow retrieve navigation state and dispatch events to native side over the bridge. We also wrap screens with redux provider that uses same instance of store.

Folders and files structure

The changes described above led us to the problem that our modules were still oriented to work only either with native or web app. But our needs were to use them everywhere so we came to the next file structure inside the module:

This new structure allowed us to have separated imports such as:

import ScreenScene from "@jobtoday/module-screen/native”

for each platform and store common parts. Each level (common, web or native) could have a components folder. We decided to organise scene files that present app screens as a combination of container and layout components. Container almost always stores in the common directory, connected to the Redux store and contains all the business logic that as props, goes to Layout. We found it very convenient to use the “render prop” approach here:

This approach defines the place where we can decide which layout, desktop or mobile we need to render.

Rolling in the deep

Using the “render prop” approach, we found ourselves passing a lot of handlers from container down to the child components which should call them. onItemSelected, onSavePressed, onTabChanged and many others were making our components hard to read and also hard to understand how every handler changed during its travel into the deep.

Here we took some ideas from redux and added localDispatch method to containers:

It allowed us to pass only one handler through all layers and call it with different event types. By calling events unique names like @SCREEN_SOME_ACTION we are able to easily find the call source and the handler.

To navigate same way

We would like to have more flexibility, which is why in native apps we implemented navigation that allows us to show either a native or React Native screen. In web app, we are using React Router. Both techs work fine for us but we also wanted to have the same API to call navigation in common components. The problem was that in native app, we needed to call a function in response to a touch event, on the other hand in web we just needed to provide a string href attribute. We created a component Link and started experimenting with props unification. Finally we came to next implementation for Link.native.js:

And Link.web.js:

As you can see both components have an identical API and can be used as follows:

<Link.Screen state={{key: 123}} />

We found several advantages by doing it this way. One main advantage is that we can specify Flow type for state that a certain link accepts. This helps us a lot during development and refactoring. Another advantage is that now we can easily find all the places where we navigate to job screen just by searching Link.Job :)


It is interesting that we came up with this implementation on our way to Subway when we were loudly discussing the problem. Afterwards I found that research has shown that people are more creative when they are walking. It seems to work!

And where we came

It took 3 months to develop and refactor old parts, including summer vacations. We released web and iOS almost at the same time and Android, as usual, for React Native projects came after. The biggest reward for us is that our main KPIs weren’t affected in any negative way after release. More over we can improve some metrics on web by porting a better user experience from native clients and unifying UI.

Team

Now, we are 8 developers and each of us can implement a new feature or fix a bug in common code. We’ve got the interchangeability that we wanted and what was so important for this project. Of course we still have some native or web parts which require deep platform knowledge so not every one of us can deal with it. But in most cases each of us can solve daily basis tasks, do code review or build a new version.

Code sharing

It is hard to count precisely the proportion between common code and platform specific code. But to show how much approximately we share I took the most popular screen in our service, job screen, and count lines of JavaScript code for common, web and native parts.

  • ~7000 LOC of common code (components, actions, reducers, selectors)
  • 200 LOC for native part
  • 850 LOC for web part

Even though the numbers may not be very accurate, the proportion of common code is an order of magnitude larger.

Impressions

In the first sprint after we released common code base, one of our product tasks was to add a similar jobs list to the job screen in both of our apps and web. This was done by one person in 4 days, easy and effective. This is what we were going for.

Of course there cannot be only advantages, having bugs in common code will lead to bugs across all 3 platforms. And sometimes common code boundaries add extra complexity in certain solutions. But we are aware of this trade off and think these are not unsolvable problems.


We are still getting used to working with common code base: looking for better practice to organise testing and deploy processes, optimising web experience, choosing the right balance between native, react-native and web. We still have a lot of things to do. But I think we now have a good base to work on and it will help us grow as team and company.

I look forward to sharing more about our experience and best practices of living with common code for native and web in upcoming stories.