My new toy lately has been React Native, the ambitious project by Facebook to bring iOS and Android development into the same codebase using JavaScript and ReactJS as the syntax of choice. This means that web developers already familiar with React have the special opportunity to do what they’ve probably wanted to do for a very long time: build native apps!
To me this is a “Holy Grail”: being able to write in one language and target many platforms.
Well, it took me long enough to get around to trying it out. Now that I’ve finally had a chance to sit down and give it a proper go, I’d love to share the things I’ve loved and hated so far and provide some advice for those wanting to get started with React Native.
Note: I’m running on macOS, so some of what I cover may not be relevant or possibly even functional in Windows or Linux, especially anything relating to Xcode or iOS. Sorry!
This is Part 1 of my first technical series: React Native for Beginners. I’ll be documenting my journey with the framework and giving as many tips and as much advice as possible as I go along. Do check back often to keep an eye out for the continuing parts!
Create React Native App
The recommended way to get started with a React Native project is to use create-react-native-app, as per the React Native documentation’s Getting Started guide. This CLI tool will setup a local project on your computer that comes pre-loaded with all the configuration and scripts needed to test, build and run a React Native app with full support for Expo’s tooling. This is the approach I first took.
If you’ve read my previous post about zero configuration JavaScript builds, then you’ll know that I’m a big fan of using CLI tools to automate as much of the project setup process as possible.
Installation is painless, though do note if you’re using a cutting-edge version of Node, you may find that create-react-native-app
chokes and tells you to downgrade or use Yarn. I didn’t mind giving Yarn a go for the first time, so I installed it globally:
npm i -g yarn
And was then able to successfully setup a new React Native project using Yarn instead of NPM. Here’s a cheeky shortcut command that means you won’t need to install CRNA globally first:
yarn create react-native-app my-new-project
Then you’ll have a new directory created called my-new-project
containing all the basic boilerplate setup to get your app rolling. Go ahead, fire her up:
cd my-new-project yarn start
After a little warm-up time, a QR code should display in your terminal and you can scan this with the Expo app on your smartphone. And there you go— your app is now effectively running on your device!
The cost of using create-react-native-app
If you’ve googled around about React Native, you may have seen a toolkit called Expo being mentioned. This is an open source development toolset designed to get up and running really quickly with React Native app development. You can sign up, install an app on your smartphone (iOS or Android) and create an app with CRNA (Create React Native App) that you can preview on your phone within minutes. And there’s no configuration required on your part, because CRNA comes pre-bundled with all the configs it needs to work with Expo immediately. This is mobile development Zen.
I carried on using the setup that CRNA gave me for a few hours— I already had a rough design and an idea for what I wanted to build. Development was pretty swish and I loved seeing my changes sync to my phone almost immediately. After all, that’s what us web dev folk have come to expect from our tools. Using ReactJS syntax with CSS via React Native’s exposed StyleSheet
function made development very familiar and enjoyable for me. No complaints.
I then hit an issue: I wanted to apply a linear gradient to the background of a View
component (you can think of a React Native <View>
like a <div>
in web development). However, currently it’s not possible to use CSS gradients within the React Native stylesheet.
After a quick google, I found the module react-native-linear-gradient did exactly what I wanted. That’s when I discovered a critical limitation of using create-react-native-app
.
You can’t install modules that rely on native code.
The reason is because modules such as this that rely on some native code to be linked into your app’s existing iOS and Android native code can’t be linked. CRNA abstracts away all native code in your project, leaving you helpless if you wish to add anything native.
Therefore, I couldn’t install this one relatively small module.
This is one sad caveat that you wouldn’t see with ReactJS web projects created via the similarly named create-react-app
CLI, because in web development we only write for one platform: the web browser.
Time to eject!
Luckily, using a project generated by create-react-native-app
doesn’t lock you in for good. You can at any time run this command:
yarn run eject
And boom! You’re no longer bound by CRNA’s limits and your project folder will fill with all of the folders and files that were previously hidden from you, giving you total project control. Yes, even the native iOS and Android directories will appear. So, that’s what I did.
Running the eject
command presented me with this prompt:
How would you like to eject from create-react-native-app?
I was then able to choose between either ejecting the app out to a “regular React Native project”, or to a project that still uses “ExpoKit”. The latter meant that I could still use Expo’s tools to develop the app, so that’s what I chose.
Project setup doubts
After ejecting my app from the CRNA tie-in, I started having second thoughts about continuing with this project setup. Here’s the main reasons why:
1. I was tied to using Expo’s fork of React Native
This isn’t necessarily a bad thing, but it’s a dependency I didn’t really need. After some playing around, I found that what was comfortable for me was mainly using the iOS device simulator on the Mac to preview my work. That doesn’t require Expo to work.
Another thing to bear in mind is that the Expo fork of RN is also a bit behind the current latest release of the official React Native build version. When I setup my project it grabbed version Expo v25, which was forked from React Native v0.52. The current release of React Native is v0.54. Not miles ahead, but enough to make me take notice.
2. CRNA isn’t the only way to bootstrap a React Native project
I found out shortly after setting up my original project about another tool— react-native-cli that has an init
command. This is similar to running yarn create react-native-app
as it will create a project directory and populate it with a good set of default files. The upshot is that it’s not tied to Expo at all and you always have full control over your project’s setup and dependencies.
This means that installing modules with native platform code in them should be straightforward from the get-go.
The funny thing is that CRNA still uses a local copy of react-native-cli
to run the commands responsible for previewing on iOS and Android. This just adds to my worries of having an unnecessary abstraction layer.
3. I couldn’t find a good source of information about the real pros/cons of using Expo
For me, I only want to use tools and dependencies that I know I need to serve a particular purpose and being a heavy user of NPM has taught me over the years to be darn cautious with the 3rd party assets I chuck into a project.
After reading through Expo’s SDK documentation, I didn’t feel any the wiser about whether it offered real benefits for RN app development in my case. They say they include helpers and their own components to get at native platform-specific hooks, but these can almost certainly be accessed with other means or modules. I don’t want to buy in to a big collection of components I may never even know about, let alone touch.
The final nail in the coffin is that I’ve spoken to a good number of people who’s opinions I trust over the past couple of weeks and upon mentioning Expo I haven’t heard much praise for it at all.
Paulsc’s React-native first impressions Medium post echos a lot of my feelings too about using CRNA/Expo over going the more “pure” react-native-cli
route.
React Native CLI
I ditched my create-react-native-app
& Expo setup that was giving me unease and dived in once more, this time with react-native-cli
, like this:
npm i -g react-native-cli react-native init mynewproject
Do note that you don’t add -cli
to the end of react-native
when you reference the CLI’s commands. Also note the lack of hyphens now in the project name— the CLI won’t allow them.
I didn’t tinker with any config in my previous project attempt, so I easily copied over all the JavaScript source files I created into the new project directory and I was back in business in no time at all.
Adding convenience scripts
The commands required to get it running on an emulator, as mentioned above, are the same as those provided with CRNA, although you’ll need to call them directly from react-native: react-native run-ios
, or create yourself some helper NPM scripts inside your package.json:
"scripts": { "rn": "node node_modules/react-native/local-cli/cli.js", "start": "npm run rn start", "ios": "npm run rn run-ios", "android": "npm run rn run-android", "test": "jest" },
The rn
script calls the locally installed react-native-cli
command, similar to how CRNA worked.
Running your app locally
To start with, run:
yarn start
That fires up a local server that will bundle your JS and make it available to any listening devices/emulators. Once that’s running, you’re then free to run either or both of the platform run commands in another terminal window/tab:
yarn run ios yarn run android
It’ll take a bit of time on the first compilation of each platform, so be patient whilst it spins up. It might only be on the second time running the command that it works, too, so try again if it fails once.
On macOS, iOS is the generally easier of the two to get running in a simulator. Do note you’ll need Xcode installed and updated first. Assuming you have Xcode already installed and have ran it once (to accept the T&Cs), the run command will automatically open an iPhone simulator for you.
I highly recommend you reading through the official React Native Getting Started guide under the Building Projects with Native Code section. Select your OS and target platform and you’ll be given very detailed information about how to get set up correctly. This page was an absolute lifesaver for me!
One thing to note about building for Android
I had an issue building for Android that was possibly due to an issue with the way the React Native CLI produced the boilerplate files for Android. The error I saw when running yarn run android
was as follows:
> Could not resolve all files for configuration ':classpath'. > Could not find com.android.tools.build:gradle:3.0.1.
Mercifully, the fix was a small file change and didn’t take me long to find via a StackOverflow question, which is here if you want to check it out for yourself. Essentially though, here’s the fix you need to make inside android/build.gradle
:
buildscript { repositories { jcenter() google() // <- add this mavenCentral() // <- add this } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' } }
After adding those 2 lines, it should build successfully.
Writing unit tests
I was eager not only to code, but to write tests, too. What can I say? I’ve been long bitten by the unit testing bug.
The recommended testing suite is, unsurprisingly, Facebook’s own JavaScript testing toolset Jest. If you’ve suffered using an earlier version before and are sceptical— don’t worry, it’s far better than it used to be and I’d argue that it’s overtaken other popular options such as Mocha or Ava. It’s great for web, and it’s great for React Native, too.
One sticking point that I’m still gritting my teeth ever-so slightly over is that a lot of NPM modules will throw ugly errors when you try to run Jest.
Jest has a documentation page— Testing React Native Apps— that I’d strongly recommend reading. It sheds light on why weird errors may be thrown from some imported NPM modules, like import
being undefined.
The trick is to add a transformIgnorePatterns
field to your Jest configuration (either inside your package.json’s jest
options, or inside a dedicated jest.config.js
file) and add those offending NPM package names. Here’s how mine is setup currently:
const transformExclude = [ 'react-native', 'react-navigation', 'react-native-safe-area-view', 'react-native-linear-gradient', 'react-navigation-redux-helpers' ].join('|'); module.exports = { preset: 'react-native', transformIgnorePatterns: [`node_modules/(?!(${transformExclude})/)`] };
As you can see, there’s quite a few modules I’ve added to the ignore pattern. Doing this means that Babel will not ignore those modules and will work its magic on them to allow Jest to execute them just like the rest of your source code.
It’s a crappy solution really. But, it works and that’s what Jest’s own docs recommend us to do.
One thing I always turn to in ReactJS unit testing is AirBnb’s enzyme, because it makes testing components very easy. They give some documentation saying it can work with React Native, however I had no luck getting this to work no matter what I tried, so I stuck to using the more “primitive” react-test-renderer
. It’s a bit fiddly, but it works.
Here’s a basic test from a Button component I wrote:
import React from 'react'; import renderer from 'react-test-renderer'; import { Text } from 'react-native'; import Button from './Button'; function render(props = {}) { return renderer.create( <Button title="Test" {...props} /> ); } test('title', () => { const textNode = render({ title: 'Test Title' }).root.findByType(Text); expect(textNode.props.children).toBe('Test Title'); });
Conclusions
I only very briefly mentioned it above, but I’m really enjoying working with React Native so far. The familiarity factor for me is one thing, but knowing that everything I’m writing can be shipped to the two most popular smartphone OS’s is bloody marvellous. I know I’ve barely scratched the surface yet and I’m pretty sure I’ll come across new obstacles that I’ve never encountered before, but that’s the nature of the beast.
It’s not flawless, but let’s not forget: React Native’s release is currently at v0.54— it’s not even a stable v1.0.0 yet! If you buy into it, you’re also buying into the time it takes to debug more issues than you’d typically encounter with a long-standing, battle-proven language or framework.
As for testing, I still feel there’s lots of improvements to be made when it comes to unit testing React Native apps. Although Jest is a great test runner, the rest of the tools feel clunky and out of place. I’m considering spending some time to create a mini toolchain that’ll provide the kind of intuitive component testing methods that Enzyme gives React JS projects. I’d also be keen to find a more robust/scalable solution to ES7+ syntax in NPM packages throwing errors.
If there’s one major takeaway from the experience I’ve had so far with React Native, I’d say: don’t bother with CRNA (Create React Native App). Choose React Native CLI (react-native-cli
) and don’t look back. It provides a good, solid starting point via the react-native init
command without compromising on what your app is capable of.
In Part 2 of this series of posts exploring React Native, I’ll start adding some pages and components. I might even add some state management, a la Redux.