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 […]
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.
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.
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!
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.
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.
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.
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.
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.
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.
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!
Sharing code between web & React Native: Why & how to configure Metro for code sharing was originally published in Affinity on Medium, where people are continuing the conversation by highlighting and responding to this story.