The following article is a part of The Ultimate Guide to React Native Optimization and describes how to improve your apps by Continuous Integration (CI).
Why is it important?
A lack of working Continuous Integration may seriously harm your apps on many different levels. It can slow down the development process, testing, and even can expose your final app to bugs and regressions that may decrease your income. In this article, I'll show you how to avoid these unpleasant situations with Continuous Integration.
In the previous parts of our guide, we have discussed:
- Reducing the device’s battery usage with UI re-renders
- The best practices around using dedicated higher-ordered React Native components
- Picking external libraries
- Libraries optimized for mobile
- How to Achieve 60FPS Animations in React Native
- Why Is It Essential to Always Run The Latest Version of React Native?
- How to Debug Faster and Better with Flipper
- Why Is It Worth to Automate Your Dependency Management with autolinking?
Working Continuous Integration (CI) in place.
Issue: Lack of CI or having an unstable one means a longer feedback loop - you don’t know if your code works and you cooperate slowly with other developers.
As you have already learned from the previous articles, covering your code with tests can be very helpful for increasing the overall reliability of your app. However, while testing your product is vital, it is not the only prerequisite on your way to shipping faster and with more confidence.
What is equally important is how quickly you detect the potential regressions and whether finding them is a part of your daily development lifecycle. In other words – it all comes down to the feedback loop.
For better context, let’s take a look at the early days of the development process. When you’re starting out, your focus is on shipping the first iteration (MVP) as fast as possible. Because of that, you may overlook the importance of the architecture itself. When you’re done with the changes, you submit them to the repository, letting other members of your team know that the feature is ready to be reviewed.
While this technique can be very useful, it is potentially dangerous on its own, especially as your team grows in size. Before you’re ready to accept a PR, you should not only analyze the code but also clone it to your environment and test it thoroughly. At the very end of that process, it may turn out that the proposed changes introduce a regression that the original author hasn’t spotted.
The reason for that is simple - we all have different configurations, environments, and ways of working.
While relying on the same configuration, similar principles of the development, and attention to details helps move faster at the early stages, it may result in shipping something that breaks the tests.
It’s harder to onboard new members to your organization. You can’t ship and test PRs and different contributions as they happen.
If you’re testing your changes manually, you’re not only increasing the chances of shipping regressions to production. You’re also slowing down the overall pace of the development. Thankfully, with the right set of methodologies and a bit of automation, you can overcome this challenge once and for all.
This is when Continuous Integration (CI) comes into play. CI is a development practice where proposed changes are checked-in to the upstream repository several times a day by the development team. Next, they are verified by an automated build, allowing the team to detect changes early.
The automated builds are performed by a dedicated cloud-based CI provider that usually integrates with the place where you store your code. Most of the cloud providers available these days support Github, which is a Microsoft-owned platform for collaborating on projects that use git as their version control system.
CI systems pull the changes in real-time and perform a selected set of tests, to give you early feedback on your results. This approach introduces a single source of truth for testing and allows developers with different environments to receive convenient and reliable information.
Using a CI service, you can not only test your code but also build a new version of documentation for your project, build your app, and distribute it among testers or releases. This technique is called Continuous Deployment and focuses on the automation of releases.
Solution: Use a CI provider (such as CircleCI) to build your application. Run all the required tests and make preview releases if possible.
There are a lot of CI providers to choose from, with the most popular being CircleCI, Travis, and the recently released Github Actions. For the purposes of this section, we have selected the CircleCI.
It is the default CI provider for React Native and all projects created by its Community. In fact, there is actually an example project demonstrating the use of CI with React Native. You can learn more about it here. We employ it later in this section to present different CI concepts.
<p-bg-col>Note: A good practice is to take advantage of what React Native / React Native Community projects already use. Going that route, you can ensure that it is possible to make your chosen provider work with React Native and that the most common challenges have been already solved by the Core Team.<p-bg-col>
With most of the CI providers, it is extremely important to study their configuration files before you do anything else.
Let’s take a look at a sample configuration file for CircleCI, taken from the mentioned React Native example:
Example of .circleci/config.yml.
The structure is a standard Yaml syntax for text-based configuration files. You may want to learn about its basics before proceeding any further.
<p-bg-col>Note: Many CI services, such as CircleCI or Github Actions, are based on Docker containers and the idea of composing different jobs into workflows. Github and its Github Actions is an example of such provider. You may find many similarities between those services.<p-bg-col>
There are three most important building blocks of a CircleCI configuration: commands, jobs, and workflows.
Command is nothing more but a shell script. It is executed within the specified environment. Also, it is what performs the actual job in the cloud. It can be anything, from a simple command to install your dependencies, such as <rte-code>yarn install<rte-code> (if you’re using Yarn) to a bit more complex one <rte-code>./gradlew assembleDebug<rte-code> that builds Android files.
Job is a series of commands - described as steps - that is focused on achieving a single, defined goal. Jobs can be run in different environments, by choosing an appropriate Docker container.
For example, you may want to use a Node container if you need to run only your React unit tests. As a result, the container will be smaller, have fewer dependencies, and will install faster. If you want to build a React Native application in the cloud, you may choose a different container, e.g. with Android NDK/SDK or the one that uses OS X to build Apple platforms.
Note: To help you choose the container to use when running React Native tests, the team has prepared react-native-android Docker container that includes both Node and Android dependencies needed to perform the Android build and tests.
In order to execute a Job, it has to be assigned to a Workflow. By default, Jobs will be executed parallelly within a workflow, but this can be changed by specifying requirements for a Job.
Workflow contains jobs that can be grouped to run in a sequence or in parallel
You can also modify the job execution schedule by adding filters, so for instance a deploy job will run only if the changes in the code refer to a master branch.
You can define many workflows for different purposes, e.g. one for tests that would run once a PR is opened, and the other to deploy the new version of the app. This is what React Native does to automatically release its new versions every once in a while.
Benefit: You get early feedback on added features, swiftly spot the regressions. Also, you don’t waste the time of other developers on testing the changes that don’t work.
Github UI reporting the status of CircleCI jobs, an example taken from React Native repository.
By spotting errors beforehand, you can reduce the effort needed to review the PRs and protect your product against regressions and bugs that may directly decrease your income.
To wrap everything up, Continuous Integration can help to improve React Native apps at many different levels. It can help you to avoid long feedback loops that slow down your development and testing process.
Also, you can spot any errors faster so you don't waste time testing something that doesn't work. All this allows you to provide your customers with a better product, which increases your chances of a higher income.
Interested in improving your React Native app? We are the official Facebook partners on React Native. We’ve been working on React Native projects for over 5 years, delivering high-quality solutions for our clients and contributing greatly to the React Native ecosystem. Our Open Source projects help thousands of developers to cope with their challenges and make their work easier every day. Our React Native development company offers a wide range of services.