Over the past year, we’ve added an abundance of new features to our mobile app at Affinity. Over time, we realized that this resulted in a lot of copied front-end logic as well as TypeScript type files between our web and mobile codebases. As we worked to re-organize our codebases to share files between the them, we ran into a number of issues configuring Metro, which is the JavaScript bundler for React Native. If you’re looking for tips on how to configure Metro to allow seamless code sharing, we hope this post helps!

Image adapted from: https://gbksoft.com/blog/compare-reactjs-and-react-native/

The problem: code duplication and bugs!

As with many modern companies, at Affinity we support multiple platforms. We have a web app, a mobile app, and a Chrome extension. Since all of our products are built in JavaScript (and more recently in TypeScript), and primarily in React and React Native, a common theme has been: how do we share code effectively among these different codebases?

For our Chrome extension, the answer was pretty straightforward because both the web app and Chrome extension use the same bundler: webpack. Also, since Chrome extensions are written to operate on the standard HTML DOM, we were able to re-use a lot of our React components from our web app. We made sure to configure code sharing from the very beginning of our work on the Chrome extension.

On mobile, on the other hand, we use React Native, which uses native UI components rather than HTML elements for rendering and only allows styling to be in-line. Because of these fundamental differences from web, we didn’t bother to setup seamless codesharing for our mobile repo.

As our product evolved and we started using newer technologies like TypeScript, we started feeling the pain of not being able to share code across web and mobile. We ended up copying over type files (for our backend API responses) and complex frontend logic code into mobile. Over time, these copied files became stale. For example, if an engineer made a change to our API, they’d sometimes update the corresponding type declarations for web but forget to update them in our mobile repo, leading to bugs in our mobile app.

What solutions did we consider in order to share code between web and mobile?

1. Webpack and Haul

For our Chrome extension, we use webpack and npm to configure our web repo as a dependency so we can reference its files. We added web as a node module and then configured webpack to allow imports from that node module. React Native uses a different bundler called Metro, but there’s a webpack-based alternative called Haul that’s built for React Native projects.

Although this solution might have helped us to solve our problems around code duplication and bugs, it risked introducing its own problems. For one, Haul is not part of the out-of-the-box React Native tooling, so when updates to React Native are released, there’s a risk of incompatibility. In addition, even if we used this approach to reference our web repo as a node module, importing it at a specific commit hash, this approach could result in stale code if we didn’t update the commit hash often enough. Whenever we did get around to updating the commit hash, especially after a breaking change in our web repo, we might still run into bugs.

2. Move our mobile repo into the web repo

There’s lots of online guides to walk you through how to share code between React Native and React by setting up your project as a monorepo, which is simply a repository that contains more than one logical project (e.g. web and mobile). These logical projects are most commonly nested under one common directory (often called packages ) because it makes dependency management easier. One drawback of this structure is that because all the codebases are essentially combined, things like integration tests and builds can be a bit more challenging to setup. Other workflows, like sharing code, are easier, because every package belongs to the same repository and follows the same structure. Coordinating a large-scale refactor can also become easier. An API change that affects multiple parts of the codebase can be done in a single pull request.

There’s great tools available to help manage a monorepo setup, like Lerna, git submodules, yarn workspaces, and Bit. Here’s a great article to help you setup your project structure using one of these tools.

For our own purposes, we wanted to avoid the large time investment that would have been required to implement a large structural change to our main web repo, so we decided to divert slightly from the classic monorepo structure where all the projects are nested under a common directory. Instead, we decided to have a parent-child project relationship, with our web app as the parent and our other projects (mobile and our Chrome extension) as children. If you’re starting fresh, you might want to go with a more classic monorepo structure — we recommend referencing the article above to decide which tools you should use for managing it!

Now, we had to figure out a way to make imports work with our parent-child directory structure. This forced us to do a deep dive into Metro, which is the JavaScript bundler that React Native uses by default.

Our game plan

Here’s the overall strategy we used:

1. Move our mobile repo to be a sub-directory in the main web repo using steps outlined here. We made sure that both projects continued to build and deploy successfully, and removed all the obsolete git files ( .gitattribues , .gitignore , etc).

2. Reference a file from web in the mobile project. This required making configuration changes to rn-cli.config.js in the mobile folder — see the next section for details.

3. In our mobile codebase, start referencing web files for shared code (e.g. type files) and deleting the corresponding, duplicate files from our mobile repo.

Configuring our mobile repo for code sharing

The first step from our game plan was very straightforward and took only a day or two to implement and test. Then, we came to the hard part: referencing a file from web in our mobile repo.

Looking at the Metro config, we found that there were relevant parameters listed under Resolver Options. In order to import files from outside the React Native project, we would have to enable the resolver in Metro to look at and resolve the files in the web directory containing JavaScript files. Here is a list of some of those options and what they do:

projectRoot : The root folder of your project.

: The root folder of your project. watchFolders : Specify any additional (to projectRoot) watch folders, this is used to know which files to watch.

: Specify any additional (to projectRoot) watch folders, this is used to know which files to watch. extraNodeModules : Which other node_modules to include besides the ones relative to the project directory. This is keyed by dependency name.

Our mobile javascript assets were contained in mobile/src/ and our web javascript assets were contained in assets/javascripts . Our goal was to reference something from assets/javascripts/util or assets/javascripts/types in mobile/src .

Realization #1: Configuring projectRoot

Initially, we didn’t set projectRoot , which meant that the root was mobile/ . We tried multiple configurations where we added the parent folders in watchFolders , but we still weren’t able to reference a file.

After multiple different configs, we realized that in order for Metro to resolve files outside of where the config file is placed, the projectRoot has to be set to the folder where we want to reference the assets from. In this case, that meant setting it to assets/javascripts .

Because we changed projectRoot , we also had to move our index.ios.js and index.android.js files to this directory in order for the app to build and run.

Realization #2: Configuring watchFolders

It looked like Metro was now able to resolve these files but there were issues when we tried to import a file that had a dependency. We knew that this probably stemmed from the fact that web and mobile used different package.json . After some digging, we realized that we had to modify the watchFolders config to reference both project node_module . After these changes, we were able to start referencing web files successfully.

Realization #3: Fixing imports

Now we could reference web files, but we had broken our mobile imports along the way. Because we changed the projectRoot , we broke the way our imports worked in our mobile repo. We tried to make multiple changes to the config, but none panned out. As a last resort, we converted all of our mobile files to use relative imports. Since our mobile codebase wasn’t that big, this was pretty straightforward to do using some regexes to find and replace.

Of course, we made sure to build the app and test thoroughly after these sweeping changes!

After all these changes, we ended up with a rn-cli.config.js that looked like this:

Of course, you might learn that you need to make a few small additional changes of your own based on your particular repository setup, TypeScript configuration, and so on.

Looking Forward

Since our app is quite complex and we reference these type and util files from hundreds of files, we decided to start gradually migrating these files to reference the versions from our web repository. We’re excited to be reducing our tech debt on this front, and we hope this post helps you do the same!