Lessons Learned on a React-Native Project - Full Version

Welcome to my post, dear reader, on my lessons learned on my first React-Native project.

tl;dr

  1. React-Native is not a silver bullet. No cross-platform framework is.
  2. React-Native does not shield you from needing to know about the native aspects of the project.
  3. Understanding the underlying build tools is extremely important in order to be effective in troubleshooting issues.
  4. Your debugging experience is extremely important. If you're just doing random console.log/alerts everywhere you're in for a bad time.
  5. Feature development in react-native is amazingly quick, but styling can be brittle at times.
  6. The React-Native ecosystem varies wildly in quality.
  7. Underlying dependencies can trip you up, and probably will.
  8. The React-Native dev experience is designed with Mac in mind, not Windows.
  9. Read the documentation. All of it. React-Native, React, Fetch, Gradle and XCode. All of it.
  10. I have no idea how to do effective testing in a React-Native project.
  11. If you've got native dev experience, you're at a huge advantage coming into React-Native.
  12. You probably want state management for any non-trivial project, from the start.

Whew. That's a lot of lessons. Overall, this was a fairly difficult for project for me. Not having had any experience in mobile development that wasn't Xamarin, coming into a React-Native project I realized very quickly how out of my depth I was. So, if you're still reading, great! Come with me on a journey of death and destruction exploration and discovery!

I'm going to break down each of the above points into its own section (yep, this'll be a big post) and delve into the details of why and how this lesson affected me and why it was important. I'll avoid code and in-depth technical lessons for the most part, as that isn't the focus of this post. Those will likely be separate posts, but linked back to this one.

Before we get started though...

Caveats

  • I have no idea what I'm doing. Seriously. Even now, with this blog post, I just have slightly less of no idea what I'm doing.
  • I'm probably wrong with a few things. I've likely made assumptions or statements that anyone with knowledge of React-Native (or the respective native platforms) will probably go "uhhh... what?". If so, please yell at me politely on Twitter, and I'll add some updates where necessary.
  • Some of these issues were greatly exacerbated by my lack of any native mobile development knowledge coming in. Up until this project, most of my mobile development experience came in the form of Xamarin, which is the .NET variant of cross-platform mobile development.
  • I'm not covering Expo as part of this post. The main issue with Expo was as soon as you need to hook into anything not covered by its libraries you need to "eject" from the Expo project and then you're in the wilderness of React-Native anyway.

Lets get started!


React-Native is not a silver bullet.

To be honest, this lesson applies to any cross-platform library, and is my biggest takeaway from this project. Going into it I'd built up React-Native in my head as the solution to all my cross-platform problems. I mean, come on, we're just writing JavaScript! And it's a simple compile to get it working across both platforms! Amaaaaaaaazing!

... Yeah nah mate.

Cross-platform development is bloody hard. Here's what cross-platform framework developers are trying to accomplish;

  1. Present something familiar (JavaScript/React, C#.NET/XAML) and map it to concepts relevant to mobile development.
  2. Write APIs to essentially map the things we're familiar with into the underlying native components that will actually be rendered.
  3. Abstract away several build and/or dependency systems, and have it all magically "just compile".
  4. All while being performant.

This shit is hard. Having spent a bit of time with both React-Native and Xamarin I have a huge amount of respect for what these developers are trying to do. I get super frustrated with both of these platforms (and show no bones about letting those around me know it), but that doesn't mean I don't admire what the developers have managed to do up to this point.

As a result of this complexity, you're probably going to hit some pretty crazy stuff... Incorrect dependency resolution, broken builds (that worked locally I swear!), inconsistent rendering between platforms and things you would never think can happen. I have a story on this last bit, but I'll save it for later in this post.


React-Native does not shield you from needing to know about the native aspects of the project

One of the biggest differences I've noticed between React-Native and Xamarin is the fact that React-Native has a much greater dependence on the underlying native platforms you're developing for. As it turns out, the "React-y" part of your React-Native project is actually a tiny part of your overall solution. Xamarin, on the other hand, abstracts away (successfully for the most part) a huge amount of the native ecosystem. More on this later.

When you create a React-Native project, you're given the following things...

  • The usual node-related things (package.json etc)
  • An App.tsx file, which serves as the entry point for the Application
  • An ios folder
  • An android folder

Now that may not seem like much, but underneath those two android and ios folders are two completely separate (and very complicated) ecosystems. And you will eventually need to delve into these folders, at which point (whether you like it or not) you'll start to get intimately familiar with how each of these ecosystems work, and how it impacts your project.

This means potentially you'll need to get to grips with;

Android
  • Android Studio - The default IDE for use with Android projects.
  • Gradle - The underlying tool that drives the Android dependency and compile processes for React-Native.
  • Android SDKs - Of which there are so many.
iOS
  • XCode - The default IDE for use with Apple projects, which also serves as the build tool.
  • iOS SDKs - Of which there are so many.
  • CocoaPods - A dependency manager specifically for Swift and ObjectiveC libraries. Expect to deal with this if you're having to integrate 3rd part components in your application that hook into native concerns.

And I'm not even touching on the different hardware configurations for each of these platforms. ::laughs in Android::

Oh, and while I'm talking about hardware I just want to make something very clear: Do your development on a device, not a simulator. Simulators do their best, but we had quite a few times where things didn't quite work as we'd expect. Especially with regards to styling. So if you're starting a React-Native project, I'd make sure you have at least one phone from each platform.

If you're starting to think "wow, that is a lot of stuff I need to be worried about"... Yes. Yes it is. :)


Understanding the underlying build tools is extremely important.

This is closely linked to the point above, but I felt it was worth calling out separately. In trying to (at a high level at least) abstract away the build process for each of the platforms, React-Native is attempting to plug a huge gap in your experience, for things to "just work". In practice, I found this rarely worked out in my favour.

Let me put it this way: If Gradle and XCode have entire sites dedicated to their usage, it gives you an idea of how deep you can potentially go. Or, how deep you may need to go if things go pear-shaped. As they most likely will.

This was one area where I found Xamarin is miles ahead of React-Native. Xamarin (to my knowledge) doesn't require any dealing with Gradle and XCode projects, it actually completely abstracts away the build process with a decent degree of success (tooling issues aside, which is a post for another day). React-Native however, is still using raw Gradle files, and hooking directly into the XCode xcproj or xcworkspace files and invoking the build processes directly. So while you're closer to the "metal" in terms of compilation for the platforms, it also means you need to delve deeper into those respective platforms when you're diagnosing issues with your build process.

Another thing I'd call out is be very careful when you're tinkering in those native Gradle or XCode projects. I found out the hard way what happens when you mess with things you don't fully understand... Our iOS project got into such a state where I had to rebuild the entire project from the ground up because something in our iOS dependencies was so fundamentally broken nothing built. And not just regular "it doesnt build", but if we tried to push it up to the remote build server (in trying to exclude the issue being our local environment) we got different failures.

So if you find yourself needing to tinker with header search paths, build phases, or modifying schemes in XCode I'd seriously question what you're actually trying to accomplish.

And make sure you're using source control, because when you eventually get things into such a state you cant tell up from down, it is wonderful to be able to do git reset --hard, and git clean -xfd and start from a clean (ha) slate.

There's a bit more on this later, when we talk about underlying dependencies.


Your debugging experience is extremely important.

Firstly, if you're scattering console.log(anObject) or alert('my wonderful message here') around your solution, you're in for a bad time. I know this because it was my experience for most of this project. I am not proud of this.

One of the best things to come out of the React ecosystem is that of hot-reloading and/or live-reloading. I don't know if it's been done before, but React was the first to do it well. Like, really well. I strongly recommend turning either/both of these on when you're developing a React-Native application as, especially once you get stuck into feature development, it'll save you a ton of time having to restart/rebuild the application. Magic.

One problem I had was React-Native tends to prefer the Chrome debugger by default when you enable it on the package. I'm not a huge fan of the Chrome debugger because I feel like it rarely behaves like I'd expect it to. What I ended up doing was a combination of things to get a debugging experience closer to what I prefer. My IDE of choice for React-Native choice is VSCode, so with that in mind:

  1. Install the React-Native extension for VSCode
  2. Go to the Debug tab and create a new configuration. Your VSCode instance should now pick up the fact it's in a React-Native solution and show you some relevant configurations.
  3. Pick the "Attach to Packager" option, and let it create the configuration.
  4. Start your packager, and make sure it's running with no problems (I've lost count of the amount of times I was having build/runtime issues only to eventually realise my packager was broken via some random exception).
  5. Go back to the debug menu in VSCode, and run the "Attach to Packager" configuration.

Now here you might notice that none of your breakpoints are registering correctly in the IDE, and when you try and hit the breakpoints from your application nothing works. Thats because the fun isnt over yet!

  1. Start the developer menu on your device/simulator, and click "Debug JS Remotely".
    • Its important to note that you need to do this step after attaching to the packager in VSCode, otherwise React-Native will launch the Chrome debugger and it doesn't play nice with the VSCode debugger.
  2. At this point your VSCode instance picks up on the fact the relevant package has said "hey debug me plz" and loads the breakpoints correctly.

If you do finding yourself wanting to use alert somewhere for a quick & dirty check, one method I found really useful was JSON.stringify, passing in a JSON Object. This just returns the relevant JSON as a readable string (well "readable" in a very loose sense here).


Feature development in React-Native is wonderful.

This was by far my favourite thing, which is pretty funny because the entire selling point of React-Native is fast feature development! It just doesn't mention all the ancillary stuff you'll probably need to pick up on the side!

Anyway, this was a breath of fresh air compared to the rest of the issues I'd been having. React-Native is really good at showing a consistent visual experience between the two platforms, so if this is essential to your project, I'd strongly recommend React-Native. Provided you can stomach the rest of the stuff that may come with it.

A big part of why feature development is so quick in React-Native comes down to the simplicity of the components, most of which roughly map to Web concerns (there's that familiarity again!). For example, the View component maps to a div in HTML land, and you'd use them similarly. When you actually look at the documentation for React-Native you can see there's only actually about 10 components you'll use regularly, most of which map to well-known concepts like Lists, Images, Buttons and Text.

One thing to be wary of is styling and layout, as I found it can be a bit brittle at times. To be fair, this isn't limited to React-Native, but anything that uses anything remotely like CSS. One thing in particular to watch out for in the context of React-Native is; if you introduce an enclosing View tag for whatever reason (say, nesting some components inside a TouchableHighlight as you're only allowed a single child element within this component), it's probable that styling for the component will break in ways you don't expect, as it won't always respect the outer container's style and things will vanish/move around. It's weird, but it could also come down to my lack of in-depth knowledge of CSS, Flexbox and the React-Native-flavour of these it uses.

Oh and Flexbox. Oh my god I love flexbox. It makes layouts fairly straightforward for the most part (and I'd suggest actually drawing it out if you're struggling to get things working). And if you combine that with Live Reload, you can iterate really quickly when styling up a new component. Heck yes. There's also a great resource I've used called Flexbox Froggy which is an adorable mini-game I'd highly recommend to get used to the quirks of Flexbox.

This is the main area that React-Native crushes the competition, especially when compared to Xamarin. I've found feature development (specifically styling/consistent looks between platforms) really painful in Xamarin. Styling and layout in XAML is horrible. Coming into React-Native for this particular area was a completely different world. Xamarin Forms 3 may change this opinion - as it's introduced a CSS-like syntax for styling - but I'll have to withhold judgement for the time being as I haven't had a chance to delve into it.


The React-Native ecosystem varies wildly in quality and levels of support.

Now, to be clear, this isn't to bash on the amazing folks in the community who do open-source (henceforth referred to as OSS) work. This is an observation on the state of the React-Native ecosystem in general and the issues you may encounter.

  1. There are a lot of abandoned projects out there.
    • These projects may be perfectly fine to use, especially the ones that don't need linking. Me personally though, I feel uncomfortable using a library that hasn't had any activity in ~2 years.
  2. You'll need to get used to trawling through the issues sections of GitHub repositories.
    • Because React-Native is still so young (as of writing, we're currently on 0.57!) you will encounter many and varied issues. If you're having trouble with an odd behaviour of a bug and you go to raise an issue on that repository, please be nice. Everyone is a person and deserves courtesy and respect.
  3. Sometimes the issues you encounter (and even known to the repo owners) won't have a fix.
    • You may need to rely on other people's forks of that repo, or specific branches which attempt to fix the issue that (for one reason or another) haven't made it into the master branch yet. Heck, you may need to fork a repo or two and make the changes yourself. On the plus side, hey you've just contributed to OSS! :)
  4. Be careful of React-Native libraries that only support one of the platforms.
    • This isn't something I encountered too often, but sometimes people write libraries for the specific platform they're interested in and don't really care about the other. Which is perfectly reasonable. Hell, we're lucky they're sharing their work in the first place! If you encounter something like this, and you feel that strongly about it, either raise a PR adding support, or fork and maintain your own branch that supports both platforms! The magic of OSS continues!

When starting a React-Native project, I'd have a go-to set of resources you can fall back to. Luckily, the OSS community shines again in providing:

  • React-Native-Community - A community-driven set of components for (you guessed it!) React-Native. This should probably be your first port of call when searching for a capability you don't get out of the box.
  • Awesome-React-Native - A one-stop-shop for all your React-Native needs! This is a curated list of useful resources and I wish I'd spent some more time looking at this in depth. Though some of the above caveats (around abandoned projects and frustrating bugs) apply.

You might be thinking "uhh Adam, what about the React-Native docs?". I'll get to that later in this post :) It gets its own section!

Something which people tend to forget in the context of React-Native, especially with the noise of everyone yelling "Oh its a Facebook thing it must be great!" is... React-Native isn't even at Version 1 yet. It's (at time of writing) 0.57! As a result of that you may hit some rough edges. So if you're not prepared for that possibility, you're in for a bad time. And that's just React-Native itself, let alone the 3rd party libraries that have sprung up around it.

You'll want to be mindful of this when thinking "should I start a React-Native project?".


Underlying dependencies can trip you up, and probably will.

Oof. This was, by far, the most painful part of my experiences with React-Native. God, where to start.

In React-Native, there are five main dependency concerns you need to worry about:

  1. Yarn/NPM
    • This serves as the entry point for your 3rd party libraries. You'll add the dependencies via yarn add or npm install and it'll resolve any other dependencies it needs from an NPM perspective.
  2. Linking (via the react-native link command)
    • This is the glue that binds the relevant NPM package to the native iOS and Android projects, adding various references and setting up the correct dependency resolution concerns for that platform. So things like adding extra libraries in XCode, Gradle file changes or Pod file changes.
  3. Gradle
    • This is where the native Android components of the above Yarn/NPM dependencies get defined.
  4. XCode
    • This is where the native iOS components of the above Yarn/NPM dependencies get defined.
  5. CocoaPods
    • And, because we're in iOS land, an extra layer of dependency pain. Sometimes, the libraries you're integrating don't actually contain the native code it's trying to call, and this is where CocoaPods comes in. This is basically Nuget (of .NET fame), but for Swift and ObjectiveC. This means that the library you're trying to integrate is actually just bridging to the respective CocoaPods library. You'll get to know your Podfile intimately :)
Yarn/NPM

There isn't much to say here. Quite frankly, this is the most straightforward (!) part of the process.

Linking

Linking in React-Native is a one-time thing, performed per-library when needed. When react-native link [your-library] is executed within the project, the linker goes off and does its best to add the various bits and pieces to each of the respective platforms project files.

I found linking in React-Native to be troublesome. Each library defines its own linking reference (which is what is performed when react-native link [your-library] is executed from the CLI). From what I've seen this is purely doing text manipulation on the relevant files, as opposed to using APIs on the Gradle or XCode processes to add references safely. This can lead to some really crazy problems. Oh yes, I used bold rather than italics, so you know it's serious!

Some things to note with regards to linking:

  1. Linking is not idempotent. If you re-run the command (targeting the specific library), you'll do the exact same thing to your files again, with no respect to what may have been done previously. So you'll have duplicate entries across your Gradle Files, your XCode projects, and your Pod file. Bam... build failures everywhere.
  2. Sometimes your Pod file will get entries added in a place you don't expect. One thing to be wary of in particular is which target the Pod file entry has been entered into. I had a situation where it'd unintentionally added it into a test target, and I spent longer than I'm proud of trying to figure out why the libraries wouldn't build. Once I moved the entry outside of the test target, voila! Build succeeded.

My main point of advice with regards to linking is this: If you find your link command isn't working the way you expect, or (as I found a few times) the relevant library's linker is broken somehow, follow the manual linking instructions. Seriously, there is nothing wrong with doing this. Thankfully, most libraries contain these instructions too :)

React-Native also has some doco on this, see here for iOS, and here for Android. Source control is also your friend here, if things get too out of hand.

Gradle

Ahhh Gradle.

Gradle is its own beast, with its own flaws, quirks and entire site dedicated to its usage.

The main files concerned with Gradle are:

  1. At the root of the android directory:
    • build.gradle - Used to define the high-level dependencies required by the project (so these are things like Android-specific libraries, and class-paths, defining which version of the build tools are to be used when actually building the solution.
    • settings.gradle - This is where the Android dependencies for your libraries get defined (usually referenced by relative paths to your node-modules folder). Here is also where your app (as in the underlying Java entry points) is defined in your application.
    • gradle.properties - These are project-wide Gradle settings, covering things like AAPT2, Build cache and NDK usage. We had to use enableAapt2 set to false for our project, because some of our underlying dependencies weren't compatible with this.
  2. android -> app
    • build.gradle - Wait... Another build.gradle? Yes, that's right! I'm not sure why these need to be named the exact same thing as the file in the above directory, but... here we are. This file is where SDK versions, application identifiers, and the actual dependencies are defined. If you can think of settings.gradle as the definition of your dependencies, build.gradle would be the actual call of these dependencies when building. I think. To be honest, I'm still a bit hazy on how Gradle hangs together.
  3. android -> gradle -> wrapper
    • gradle-wrapper.properties - This is what defines the particular version of Gradle that you intend to use. In CI pipelines, this package will actually be downloaded on the fly each time (I think).

One thing I found quite frustrating in dealing with Gradle is why are these files scattered all over the place? Perhaps someone with actual native Android/Gradle knowledge can answer that, but I really wish these files were all in android -> gradle, for the sake of simplicity (with some naming changes to the particular files to make the entire process significantly simpler and cleaner).

The main issues I found in dealing with Gradle were;

  1. The Android Build Tools (usually installed around the same time as you're installing the SDKs) versions are extremely confusing. There was a lot of trial and error trying to get just the right version, as things didn't always quite line up how I'd expect. You also need to make sure that the SDK version you're referencing in your Gradle file is consistent with what is installed locally, and on your CI pipeline.
  2. Your CI Pipeline will likely have a very different environment than your local build environment. This may seem obvious, but I felt in Gradle this was especially painful. Successful builds locally (that also run on device and simulator!), don't necessarily map to successful builds in the CI pipeline. You may need to spend some considerable time troubleshooting what on earth is going on.
  3. The dependencies, in terms of SDK versions, you may have defined mean nothing to your underlying dependencies for your 3rd party libraries. You may be targeting API 25, but that random camera library you've dragged in is targeting API 21 for some reason, and all of a sudden you get either weird build issues, or straight up failing builds (especially on your CI pipeline... I don't understand why, but the local environments are so much more forgiving when building!). Eventually I stumbled on a way to force all your libraries to the same SDK version you've defined:
// This goes in your top-level build.gradle file, in a separate section to the `allprojects` section you'll have. 
// This does not get nested in any other sections.
subprojects {  
    afterEvaluate {project ->
        if (project.hasProperty("android")) {
            android {
                compileSdkVersion 27
                buildToolsVersion "27.0.3"
                project.archivesBaseName = "AnApplication"
                configurations.all {
                    resolutionStrategy.force "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:customtabs:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:animated-vector-drawable:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:support-media-compat:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:support-v4:${rootProject.ext.supportLibVersion}"
                }
            }
        }
    }
}
ext {  
    buildToolsVersion = "27.0.3"
    minSdkVersion = 16
    compileSdkVersion = 27
    targetSdkVersion = 27
    supportLibVersion = "27.1.1"
}

The specifics of what you may need to define will probably vary from what I've shown above, but it'll be a good starting point for you. The build errors you'll get will point you in the direction of the libraries you need to add resolutionStrategy.force entries for.

XCode/CocoaPods

Ive combined the last two in this section because they're closely related. I came into this project knowing nothing about XCode, and as it turns out XCode is... complicated. My main point of reference for IDEs is Visual Studio, and while its far from perfect, I feel like VS makes more sense in terms of how it lays things out, defines the various bits and pieces going into a solution and how it manages external references. XCode (in my Opinion™) doesn't really make much sense. To get to certain things like schemes, or even build settings and build phases, it ended up being a non-intuitive process to get there, especially if you're trying to decipher the various tiny icons scattered around XCode. Of course, take this with a boulder-sized grain of salt as I've got significantly more experience dealing with Visual Studio :)

The one thing I will say though is the "Help" menu in XCode is excellent. Search for a term and it'll actually pop and highlight the menu you're looking for!

How XCode manages dependencies is kind of scary, and that's before we've started talking about CocoaPods. In XCode, external dependencies are managed by adding the xcodeproj for the given dependency directly into the project, under the Libraries folder in the Project Navigator view.

Then, you should be able to reference the relevant .h/.m (header and implementation files respectively) in your AppDelegate.m, where most of the things like callbacks will be handled. Annoyingly, there are multiple ways to declare header imports in ObjectiveC (I think? I'm still hazy on how this works tbh), so if you're integrating a library and when you build it starts complaining that it cant find your headers/files that are clearly actually there, I'd look at changing the particular way the header in question is imported.

The next part of complication in this story is the CocoaPods file (referred to as the Pod file). As previously mentioned, this is where some dependencies that get imported are bridging into an existing ObjectiveC library. If you're confused why we're dealing with the Libraries and this Podfile, its because Libraries are imports where you have the source locally. Think of Libraries like adding an existing project reference to another project in the same solution in Visual Studio.

Here is where things can get a little wild.

For React-Native specifically, if you get into a situation where you need to declare React-Native in your Podfile, you'll need to keep a couple of things in mind:

  1. You need to manually specify the submodules of React-Native you're interested in. I found examples of this annoyingly difficult to track down at first, but as we'll find out later, reading the documentation always helps.
  2. Sometimes, you'll get an extra React target (which contains declarations and implementations of the React-Native submodules) added to your project, despite the fact it already has the individual Library entries for the React Native submodules. Then when you go to archive, it'll throw an error saying duplicate symbols! You can add a post-processing script to your Podfile to remove the target on the fly:
// this goes at the bottom of your Podfile
post_install do |installer_representation|  
    installer_representation.pods_project.targets.each do |target|
        if target.name == "React"
            target.remove_from_project
        end
    end
end  

This is a known issue, and this was the only clean way I found that helped.

Things I don't know how to categorize

Another thing I'd like to mention, and I'm still really confused by this, is that it looks like the build process undertaken by the React-Native CLI, versus the build process taken by the native IDEs is different. Different to the point where sometimes you'll get failures from the IDEs, but not when building using the React-Native CLI. This really baffles me, and I'm still not sure if it's something I've done along the way to break something, but it is definitely a thing. So yeah, if you're in XCode or Android Studio, and things aren't working, I'd jump over to the CLI and try your luck there.

Also, sometimes the repositories you encounter will contain an example application (usually extremely basic). I encountered situations where the example didn't actually line up the with documentation provided by the repo! So if you hit an issue and you know you've followed the doco 100%, I'd clone down the repo and poke around the examples provided. Usually you'll find something different (Pod files were especially guilty of this), and if you make your changes more in line with the example app, you're more likely to have success.

Closely related to this, is you may run into a situation where multiple libraries clash with each other in terms of setup and linking. Something to realize when setting up a new project is the instructions you're following are done in isolation of other libraries. So you may need to tweak things slightly, depending on your configuration. Sadly, I don't have specific technical guidance in this area as it can vary so wildly across the different libraries. Our main pain point was modifying the AppDelegate.m file in the iOS library, and we spent quite a bit of time tweaking it in order to get the various bits and pieces working together nicely. So if you're having to do something like this, I'd recommend taking a look at the documentation for the methods that the library is asking you to modify or introduce. These are normally override methods based on well-known APIs in the ObjectiveC space, so you should be able to track something down.

In general I'd advise if you find a library that does what you want, and you don't need to do any linking, I'd go with that. It'll save you some potential pain.

So as you can see there is a lot of danger when it comes to dependency management. Now that I've got more awareness of these native ecosystems and IDEs I'm more confident in troubleshooting issues when they occur, but if you're going in brand new these are things you will need to be wary of.


The React-Native dev experience is designed with Mac in mind, not Windows.

Remember that story I mentioned in the "React-Native is not a silver bullet..."? It's storytime!

As I was talking about in the "Understanding the underlying build tools ..." section, our iOS project got into such a messed up state we ended up having to rebuild the project from the ground up, re-integrate and link everything, and plop it over our broken code and let Git do its thing. Yay for source control!

Unfortunately, when I did the project rebuild I didn't specify the version of React-Native to initialize with, so it just grabbed the latest version. And sadly, this latest version was different to the version we'd been using which was working just fine on both Mac and Windows. Initially I didn't see any issues as I was happily compiling and running on my Mac and everything worked swimmingly (even the dependencies I'd been having trouble with!). Euphoric, we merged the code and high-fives were had by all.

That was until my poor colleague tried building on Windows for Android.

We spent several hours trying to narrow down on the causes of these build errors. We tried all the steps the packager was telling us, tried nuking various caches to no avail. An interesting thing to note is, when React-Native is giving you steps to try when things fail, it normally suggests Mac-specific commands/utilities. Not much help on a Windows PC.

We eventually tracked down the issue to an underlying React-Native dependency used for when building Android was now broken on Windows, and had been introduced with React-Native 0.56. Which just happened to be the version we'd dragged in from my project rebuild.

Sigh.

As timelines were already tight for this project, we didn't have time to go back and either downgrade (which comes with its own complications) or rebuild again from scratch, we decided to make a (what I like to think was pragmatic?) decision to continue developing purely on a Mac for the remainder of the project. Which then had the knock-on effect of neither of us had really used Macs before, so we had another thing to learn on the fly, along with everything else. Good times.

So yeah, everything from the error messages React-Native will throw, to the processes of React-Native developers themselves take, revolve around Mac. It was hilariously, frustratingly, eye-rakingly bad timing that we just happened to drag in something that broke Windows development, but its worth nothing that (in my opinion at least) if you're gonna do a React-Native project, do your development on a Mac.

Oh and... you know... specify the version of the libraries you're using if you have to rebuild ;)


Read the documentation. All of it. React-Native, React, Fetch, Gradle and XCode. All of it.

I have a tendency when starting a new technology to skim the doco at best, and just muddle my way through. Let me say right now that this is a bad idea when it comes to React-Native (and probably a bad idea in general now that I think about it). There is so much going on, that if you try just "muddling your way through", you will miss out on a lot of important information and context. My recommendation before starting a React-Native project is to go (at the very least) to the following pages to get a basic introduction to the various components (ha) that make up a React-Native project:

  1. React-Native
    • For... React-Native, obviously! The doco here, while a bit confusing at times in terms of how its laid out, is really good and provides a lot of great information. I'd suggest having a close look at the Styling, Layout, Networking, and FlatList sections as a start.
  2. React
    • The underlying component engine (is that the right term?) that drives React-Native. React-Native tends to assume a lot of knowledge around React itself, so I'd highly suggest getting very familiar with React. I'd pay particular attention to lifecycle methods, as incorrect usage of these can end up having some pretty severe performance implications on your application.
  3. Gradle
    • Getting familiar with the high-level ways in which Gradle works will save you so much time and suffering. Please, read this.
  4. XCode
    • Going in with at least some basic knowledge of this beast of an IDE will help make things easier.
  5. CocoaPods
    • This isn't as essential as the others, as Pod files themselves are fairly straightforward.
  6. Android Studio
    • Similar to my point about XCode, this is another beast of an IDE. I've avoided covering it as Android Studio honestly isn't too bad. It's got some issues (Gradle syncing I found was really painful and inconsistent), but things mostly make sense in terms of when you need to use the IDE, like generating app icons. You'll also want to use Android Studio if you plan on upgrading the version of Gradle that you're using. Word of warning here... This can (obviously lol) change how things compile, so you may get new errors you hadn't seen before. Unless you really need to, I wouldn't update Gradle unless you have to.
  7. React-Native CLI
    • Useful specifically for the commands around logging simulator/device logs, but also gives you some nice context around how the CLI works in general.

I have no idea how to do effective testing in a React-Native project.

This was an area where I'd started off with Really Good Intentions™. I'd decided early on we were going to try snapshot testing for our components. We bootstrapped what we needed to, and set about adding some basic snapshots. At first things were... ok at best. Honestly, we never got in the habit of running the bloody snapshot tests (let alone integrating them into our CI pipeline, but that's a separate sort of stupid I'm saving for another post). So we noticed a few things happening:

  1. When we did remember to run the snapshots, they'd inevitably fail, because usually there'd be some sort of unrelated change which (correctly) produced different output.
  2. This cycle got us in the habit of when we made a change to a component, just running the update snapshot command. Meaning sometimes there would be valid failures that we missed, and didn't catch til run-time.
  3. Given the rate at which our components were changing, the snapshots usually ended up being substantially different than before, making it difficult to catch those subtle breaking changes.

Gross.

This also led to some other interesting tidbits of experience:

  1. The raw output of these test (especially the bigger components) wasn't really human-friendly.

    • Part of the catch of doing something like snapshot tests (a form of Approval Test), is that a human needs to manually review and check what's being changed to ensure it made sense. The more complex the output, the more likely a human is to glance at it and just say "yeah its fine".
    • There are prettiers specifically for React-Native snapshot tests that can help but they only go so far.
  2. Snapshot tests don't render the output of components you've referenced inside the component.

    • For example a <MyAwesomeButton/> component inside a <MyAwesomeView/> component will just render the output of <MyAwesomeView />, leaving just the element declaration of <MyAwesomeButton/> in the output. They're very much done in isolation of the rest of the UI components, which I'm kind of conflicted about. On the one hand, snapshot tests are really quick, because of the fact they're done in isolation of everything else. The trade-off for this is a lot of the time your bugs will come because of how these components interact with each other.
    • There is no easy answer here (to my knowledge at least), and the only thing I can think of is to use automated UI tests, which run on actual devices. Which then can be extremely slow, brittle, and come with a whole host of other problems I won't get into now. I really, really don't like automated UI tests.

I've still got a lot to learn in this area, and now I've got some time to breathe I'm going to spend some time looking into this. Once I've got something solid, I'll come back to this post and edit in the link here.


If you've got native dev experience, you're at a huge advantage coming into React-Native.

This should be obvious by now, but if you've already got experience in the native platforms, you won't need to deal with the learning curves of several new ecosystems. If you're a .NET (or even any non native mobile) developer, you want to make sure you brush up on what I've talked about in this post, before having a proper crack at React-Native.

The only thing you'll probably need to spend some decent time with is React itself, and thankfully the doco is linked above. :)


You probably want state management for any non-trivial project.

This was a point I struggled to recommend when I started React-Native. I'd heard the mutterings of cranky developers who'd been burnt by the complexity (and boilerplate) introduced by state management and as I tend to follow the YAGNI approach to software development, I held off introducing any sort of state management.

For this particular project however, this was a mistake. As we knew from the start it would be a non-trivial project (ie. more than a TODO list), having state management from the start would have saved us a lot of time. Now, when I say "state management", I'm not just talking about Redux or MobX (the two main contenders currently for state management). I'd actually include the React Context API in this. It's not state management in the same way that Redux or MobX are, but it definitely helps with passing very particular types of data around.

We ended up getting into a situation where we encountered a well-known (to those actually experienced with React) issue that can emerge: Prop Drilling. We were needing to pass components/classes around everywhere and it just felt incredibly nasty. If we'd looked at MobX or even the Context API, I think we would've saved ourselves a lot of pain.


Conclusion

I'm guessing the one question you may have is "Would you recommend React-Native"?. And the answer to that question is... yes. An emphatic yes.

When the native concerns got out of the way it was a wonderful experience. Feature development is lightning quick, and being able to iterate so quickly through hot reloading/live reloading almost makes the rest of the suffering worth it. A good chunk of my pain in this project came from lack of knowledge and experience with the native platforms, and having that experience now means the next React-Native project I know what to watch out for.

This recommendation comes with some caveats, however:

  1. If you're purely a .NET dev coming from Xamarin and expecting magic, I'd think twice. This world is so different from the .NET world.
  2. You probably want at least some experience in the native space before really attempting a React-Native project. It's not essential, but as you can see above it'll help. Considerably.
  3. If you know the capabilities you're likely to need for any given project, do your research for the capabilities first before deciding to take the plunge with React-Native. Things you might take for granted in .NET (or even the native space) might not exist in the React-Native ecosystem. Or worse, may be in a fundamentally broken state.
  4. Dependencies really suck in React-Native. Be prepared for some pain.

So there we have it... My lessons learned on a React-Native project. It's been a wild, eye-opening ride and I hope my experiences help you avoid some of the pitfalls I've encountered!


1. Oh and you may notice I haven't talked about things like CI/CD in React-Native, and a few other technical bits and pieces... I honestly think that needs to be a separate post as (a common theme with React-Native) there's a lot to talk about.
2. If you're wondering "React-Native vs. Xamarin", this is a loaded question and will be a separate post.