Software Engineering Musings

Building Apps with React Native

May 23, 2020

There are a lot of articles that talk about React Native, however, they’re usually far too general and lack practicality. Sure, if you’re still deciding whether or not to use it, that might be good, but if you’re in need of a crash course or want useful information, you might be out of luck. This is my attempt to address the problem.

Pretext

Before we get started I should probably point out a few things;

  • I wouldn’t call myself an expert in React Native
  • I am not a native application developer
  • I’m presuming you have React experience

And with that out of the way, let’s roll.

Understanding Navigation

This is first because I think it’s the most important item to grasp.

It is extremely likely is that you’ll use React Navigation. Presuming you’ve used React Router then it’ll feel familiar enough, but be aware that, depending on where and how you navigate, certain components will stay mounted. From the docs:

If you are coming to react-navigation from a web background, you may assume that when user navigates from route A to route B, A will unmount (its componentWillUnmount is called) and A will mount again when user comes back to it. While these React lifecycle methods are still valid and are used in react-navigation, their usage differs from the web. This is driven by more complex needs of mobile navigation.
https://reactnavigation.org/docs/navigation-lifecycle

In practice, this means that if you go from Screen A to Screen B, and Screen B is a “nested” or “subscreen” of Screen A, everything on Screen A stays mounted.

The flow on effect is that you cannot take your lifecycle methods or useEffect for granted. With the latest React Navigation in a function component, you can use their hooks like so:

import React, { useCallback } from 'react';
import { Text, View } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import { apiClient } from 'api/';

const Component = () => {
  useFocusEffect(
    useCallback(() => {
      apiClient.fetchSomeData();
    }, [apiClient])
  );
  return (
    <View>
      <Text>Some data or whatever</Text>
    </View>
  );
};

You can achieve the same in a class component, but need to

  • add the event listener in the constructor
  • call whatever function it is on componentDidMount
  • remove it in componentWillUnmount

Managing Modals

If you’re using React Navigation, and there are no design requirements that force you to use an alternative, then use the modal implementation that comes with React Navigation.

If, however, you need to use the <Modal /> component that comes with React Native, or are using react-native-modal, then proceed with caution.

Essentially, if you have

  • the situation described above with Screen A and sub/nested Screen B
  • a modal in both components
  • the modals show when some value from redux or similar is true

Then you may run into a situation where you see both at the same time, and, for whatever reason, it seems to be more prevalent on iOS than Android.

There are two solutions I’ve been exposed to. The first, and arguably easiest, fix is to have some local state in Screen A, a boolean, that updates on focus and blur events, and is then passed in to the visible prop of the modal(s) in question. A contrived but not entirely unreal example below:

const ScreenA = () => {
  const [focused, setFocused] = useState(true);
  useFocusEffect(() => setFocused(true));
  useBlurEffect(() => setFocused(false));
  return (
    <View>
      <Modal visible={focused}>
    </View>
  )
};

The second solution is far more involved, and means changing how you decide to manage modal vibility via redux. In practice, this might mean using an enum to dictate what shows when, but would almost definitely invovle making more components, and needing to make changes to actions and thunks downstream.

As a final point on this, there’s a good chance you may need to marry the two techniques to get the desired outcome.

Make Components

It seems to me that, in the React Native world, there’s a theme of putting what are essentially components in class methods named to the effect of renderHeader, which looks, in code at least, something like the below:

class SomeScreen extends React.Component {
  renderHeader() {
    return (
      <View>
        <Text>Header</Text>
      </View>
    );
  }

  renderSomeModal() {
    return (
      <Modal visible>
        <Text>This is some modal</Text>
      </Modal>
    );
  }

  render() {
    return (
      <View>
        {this.renderHeader()}
        {this.renderSomeModal()}
      </View>
    );
  }
}

I can think of a few motivations for this, and it probably works fine in a small component, or when prototyping, but it scales very poorly, and for two reasons in particular.

Firstly, in a component that has grown over time, you could be looking at a file with 500+ lines of code. In that case, I guarantee you’ll lose your place often, and have a hard time remembering where anything is or what triggers a related behaviour to what you’re working on. Long story short, it’ll take far longer to make anything happen.

Secondly, you are going to be in world of hurt when using React Devtools. What makes React Devtools so great is you can just type in a component name and find what you’re after. This is, however, not the case if you have one big component with renderForThing methods.

Sure you can find the component eventually, but it takes time and the React Native component tree is not as clean and tidy as you’re probably used to, and the selected component will disappear after a hot reload.

Make components and save yourself hours upon hours of pain.

Environment Setup

I’ve only had to deal with this on a Mac, so if that’s not you, I’m sorry. Anyway, setting up your environment for Android development can be particularly irritating.

At the time of writing (May 2020) this should do the trick

  1. Install the AdoptOpenJDK 8 with Homebrew
brew cask install adoptopenjdk8
  1. Download and install Android Studio
  2. Add the below to your .zshrc or .bash_profile
export ANDROID_SDK_ROOT="/usr/local/share/android-sdk"
export ANDROID_HOME="/usr/local/share/android-sdk"
  1. Open Android Studio, then the AVD Manager, and configure a device

That’s about it. The additions to .zshrc/.bash_profile may vary slightly, but not by much.

Yes, you can install the Android SDK via Homebrew as well, but unless you’re already comfortable working with it via CLI, I wouldn’t recommend it.

You are at the behest of the device

If you’re developing for a client or other commercial purpose, you’ll probably have an agreement on the minimum OS supported, but perhaps not minimum device specifications.

For iOS this isn’t really an issue, Apple are in control and thus devices, even older ones, are still supported on the OS level, and their hardware — white not amazing for anything particularly cutting edge — will still definitely get the job done for 95%+ of apps.

On the other hand, Android can be far more problematic, as it’s a comparative wild west. There are an abundance of cheap, lower powered, readily available Android devices on the market, and have been for several years. Further, there are no guarantees that any of these have received software updates or that their hardware has aged well.

For a very basic app, this probably won’t present too serious an issue, but if you use secure storage on the device, such as through React Native Keychain, then you might be in for some surprises.

Imagine you want to use redux-persist to store a little state (and I do mean little) between app cycles, and you want to encrypt that data, as is good practice. That also means you need to decrypt the persisted state when the app opens.

For many, perhaps even most, devices, this won’t be an issue, but for some older hardware, the decryption may take a seriously long time, 30 seconds or greater long time. In turn this will cause redux-persist to throw an error and your app is fried, and the only way to recover is to delete any stored data on the device.

Not all React Native apps will use redux-persist, nor will they all use any form of data encryption when otherwise persisting data, but the sluggishness of older devices will still need to be accounted for.

Upgrading

At some point in your application’s life, you’ll probably want to upgrade the React Native version used, perhaps you’ll even be obligated to per a support contract.

Know this: it can be extremely painful and take quite a bit of time to do.

There’s the React Native CLI upgrade command, but as they note, it’s really intended for simple React Native apps, so you’re most likely out of luck on this one.

The most likely outcome is that you’ll use the React Native Upgrade Helper to get an idea of what needs to change, and then need to go through it by hand. How long that takes is going to be almost entirely dependent on the complexity of your project, but budgeting between a day and week of time would be wise.

Oh my, the things you’ll see

Generally speaking, if you follow good — not even best — React practices I’d say you’re going to have a good time.

Almost everything from regular React applies to React Native, and though it may not have had a major release yet, it’s still mature enough to get the job done, allow rapid development, and has a fairly vibrant community surrounding it.