Testing with React-Redux Hooks

With the rise of React Hooks, many developers have added hooks into their open source libraries. If you are using react-redux libraries, they also provided hooks such as useSelector and useDispatch as well.

Although hooks allow us to use redux with ease in React, however, there is not much documentation about unit testing with hooks. Thus, I’m going to show you how to unit test your react-redux hooks.

In class component, we can create a mock of the redux actions and states and pass them into the component for unit testing. As a matter of fact, there is not much difference in unit testing functional components with hooks. Instead of passing the mock state into the components, we can mock the implementation of `useSelector` to return state.

Imagine we have a component that looks like this:

import React from 'react';
import { useSelector } from 'react-redux';

const HelloWorld = () => {
  const user = useSelector(state => state.user);
  const skills = useSelector(state => state.skills);

  const { firstName } = user;

  return (
    <div>
      <p>First Name: {firstName}</p>
      <p>My Skills:</p>
      <ul>
        {skills.map(skill => (
          <p key={skill.id}>{skill.name}</p>
        ))}
      </ul>
    </div>
  );
}

export default HelloWorld;

We know useSelector is a function that takes a callback as the parameter. All we need to do is to mock the state and pass to the callback. The unit test would look like this:

import React from 'react';
import { render } from '@testing-library/react';
import { useSelector } from 'react-redux';
import HelloWorld from './HelloWorld';

jest.mock('react-redux', () => ({
  useDispatch: jest.fn(),
  useSelector: jest.fn(),
}));

describe('HelloWorld', () => {
  test('renders user with skills', () => {
    useSelector.mockImplementation((selector) => selector({
      user: {
        firstName: 'Tek Min',
      },
      skills: [
        {
          id: '1',
          name: 'Javascript',
        },
        {
          id: '2',
          name: 'React',
        },
      ],
    }));
    const { container } = render(<HelloWorld />);
    expect(container).toMatchSnapshot();
  });

  test('renders user without skills', () => {
    useSelector.mockImplementation((selector) => selector({
      user: {
        firstName: 'Tek Min',
      },
      skills: [],
    }));

    const { container } = render(<HelloWorld />);
    expect(container).toMatchSnapshot();
  });
});

If you check the snapshot, you will probably see something like this:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`HelloWorld renders user with skills 1`] = `
<div>
  <div>
    <p>
      First Name: 
      Tek Min
    </p>
    <p>
      My Skills:
    </p>
    <ul>
      <p>
        Javascript
      </p>
      <p>
        React
      </p>
    </ul>
  </div>
</div>
`;

exports[`HelloWorld renders user without skills 1`] = `
<div>
  <div>
    <p>
      First Name: 
      Tek Min
    </p>
    <p>
      My Skills:
    </p>
    <ul />
  </div>
</div>
`;

Great! we get the snapshot as we wanted, the components render correctly with the different redux states.

You may find another way of testing with redux hooks in Google, but I find that this is one of the simplest versions of doing it without additional libraries. Hope you find it helpful and have a nice day!

Using Custom Fonts in React Native

If you are looking for the guide to use custom fonts in your React Native apps, this is the right place for you.

Adding custom fonts to your React Native apps is not a difficult task, but there are few tweaks that you need to do to make sure the custom fonts are set up correctly. I will share with you steps by steps in details to set up the custom fonts.

Prepare the fonts

First, we need to get the fonts ready. For this tutorial, we will use DancingScript font as the custom font. If you wish to use other font, you may download them from Google Fonts.

There is one thing you may need to take note, Android will use the font’s file name as the font family, while iOS will use “PostScript name” as the font family. Let’s take a look of the example below:

Font files
Actual font files

If we use these fonts as custom fonts, we can use fontFamily: "DancingScriptRegular" in Android to show the custom font. However, this may not work in iOS if the “PostScript name” is different than the font’s file name. To check the “PostScript name” of the font, you can add the fonts to Font Book and view the details.

Dancing Script details from Font Book
Dancing Script details from Font Book

As the PostScript name for the font is “DancingScript-Regular” instead of “DancingScriptRegular”, setting fontFamily: "DancingScriptRegular" in the style will not show the custom font in iOS.

Thus, it is always recommend to rename the font file to the “PostScript name” so that we can use the same fontFamily for both iOS and Android platform. So, let us rename our fonts.

Add fonts to assets

Next, we need to add the fonts to the assets. Create the directory assets/fonts and put all the fonts into the directory.

If you are using React Native >= 0.60

Since rnpm is deprecated, we will use the new method to link the assets. Create a file react-native.config.js and put the following codes:

module.exports = {
  project: {
    ios: {},
    android: {},
  },
  assets: ['./assets/fonts/'],
};

If you are using React Native < 0.60

You may add the following codes into your package.json:

"rnpm": {
  "assets": [
    "./assets/fonts/"
  ]
}

Link the font assets

Once the fonts is in the right place, we can link them by running the follow command:

react-native link

To verify if your fonts is correctly linked, you can check if the fonts is copied to android/app/src/main/assets/fonts and added to Build Phases > Copy Bundle Resources of your iOS project.

Use the custom fonts in styles

Now you can use the font in your React Native app, just add fontFamily: YOUR_FONT_NAME to your Text or TextInput component and it will show the text and placeholder in custom font. I highly recommend always create a shared custom Text and TextInput in your React Native app so that you can apply the style to all the text and input in your app directly.

Bonus: Unlinking the fonts

Unfortunately, there is not command that can the fonts automatically, you will have to do it manually.

  1. Remove the fonts in assets/fonts.
  2. Remove the fonts in android/app/src/main/assets/fonts.
  3. Remove reference of fonts in Resources of your iOS project. (This will remove the reference in the Build Phases > Copy Bundle Resources too.)

Thank you for reading, I hope you find this tutorial useful!

React Native Searchable FlatList with React Hooks

If you have been using a mobile app or building one, you must be very familiar with the content of a very long list such as merchandise products, contacts, friends, countries, etc. It is very inconvenient for the user to find the required record by scrolling the whole list. Most of the mobile apps will provide a search bar for the user to search for their desire record for better user experience.

In React Native, FlatList is the common component that you will use to build the long list. FlatList can render the long and performant list just with 2 basic props: renderItem and data. If you wish to use sections, you may use <a rel="noreferrer noopener" aria-label="SectionList (opens in a new tab)" href="https://facebook.github.io/react-native/docs/sectionlist&quot; target="_blank">SectionList</a>.

For this tutorial, we will build a basic searchable country list similar to the GIF below and the solution will be demonstrated using React Hooks.

Country Selection List from Spendie Mobile App
Country Selection List in Spendie (iOS & Android)

Without any further ado, let’s get started!

Building The List

First, let’s build the UI of a search bar and a simple list of countries.

import React, { useState } from 'react';
import { View, Text, FlatList } from 'react-native';
import { SearchBar } from 'react-native-elements';
import { countries } from 'countries-list';

// Get the values of the countries and sort is alphabetically
const countryList = Object.values(countries)
  .map((country) => ({
    ...country,
    lowerCaseName: country.name.toLowerCase(),
  }))
  .sort((a, b) => a.name > b.name);

const SearchableFlatList = () => {
  const [query, setQuery] = useState('');
  const [filteredCountryList, setFilteredCountryList] = useState(countryList);

  return (
    <View>
      <SearchBar
        placeholder="Search your countries..."
        onChangeText={setQuery}
        value={query}
      />
      <FlatList
        keyExtractor={(item, index) => `${index}`}
        data={filteredCountryList}
        renderItem={({ item }) => <Text>{item.name}</Text>}
      />
    </View>
  );
};

export default SearchableFlatList;

From the code snippet above, we are using open source <a rel="noreferrer noopener" aria-label="countries-list (opens in a new tab)" href="https://github.com/annexare/Countries&quot; target="_blank">countries-list</a> to get the list of countries. Due to countries is a key-value object, thus, we need to use Object.values to get the array of the country. You must be curious why we need to add lowerCaseName for each country. The reason is that we may want to ignore the cases when we search the list. Thus, by adding the lowerCaseName, we can use it to make our search case-insensitive. Last but not least, we also sort them alphabetically according to the name.

Then, we create a state filteredCountries for holding the filtered country list and set the initial value to the countryList. Next, assign the filteredCountries to the FlatList for rendering.

For the search bar, we can use awesome SearchBar component from <a rel="noreferrer noopener" aria-label="react-native-elements (opens in a new tab)" href="https://react-native-elements.github.io/react-native-elements/docs/getting_started.html&quot; target="_blank">react-native-elements</a>. After that, we create another state query and set it when user changes the input of the search bar.

Until now, we have a search bar and a list of countries display on the screen. However, there is still no logic to search the country list when the search bar’s value is changed. Let’s add the search logic now.

Search the List

We have all the UI set and we can add the search logic now.

useEffect(() => {
  const lowerCaseQuery = query.toLowerCase();
  const newCountries = countryList
    .filter((country) => country.lowerCaseName.includes(lowerCaseQuery));

  setFilteredCountryList(newCountries);
}, [query]);

Now, we will use the effect hook and put query as the dependency of the hook. By setting the dependency, the effect hook will only be called when the query is changed. Similar to what we did previously, we will create a lower case version of the query so that we can compare with the lower case version of the country’s name. We use includes to filter the country that contains the query in its name. Don’t forget to import useEffect in your file!

Now, you can run your React Native app and you should able to search the country list when you type in the search bar.

Sorting the Result

Sometimes, for better user experience, you may want to sort the result based on the search queries. For example, if user type o in the search box, you want to display the country’s name that start with o at the top of the result. To achieve this, we can rank our result based on the indexOf the query in the country’s name and sort them accordingly.

useEffect(() => {
  const lowerCaseQuery = query.toLowerCase();
  const newCountries = countryList
    .filter((country) => country.lowerCaseName.includes(lowerCaseQuery))
    .map((country) => ({
      ...country,
      rank: country.lowerCaseName.indexOf(lowerCaseQuery),
    }))
    .sort((a, b) => a.rank - b.rank);

  setFilteredCountryList(newCountries);
}, [query]);

Using Debounce

Besides ranking the result, there is another enhancement we can do for the search, It is not wise if we try to search the result while the user is still typing because you may need to do a lot of processing and may create flickering on the screen. If you are calling API for the filter/search, you will have a lot of redundant calls and handling for race condition. To fix the problem easily, we can use debounce to wait until the typing action is finished before we proceed to search the result. It will create a more performant and responsive experience for the user. To understand more about debounce, you may read this tutorial.

Let’s create a simple debounce hook.

const useDebounce = (value: any, delay: number) => {
  const [debounceValue, setDebounceValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounceValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debounceValue;
};

Next, we will debounce the query for 300 milliseconds. It will gives the user a 300 milliseconds window to type the next character before we start to search the result.

const debounceQuery = useDebounce(query, 300);

Lastly, we change the effect hook’s dependencies from query to debounceQuery.

useEffect(() => {
  const lowerCaseQuery = debounceQuery.toLowerCase();
  ...
}, [debounceQuery]);

By adding the debounce, we can save some processing power when user is typing in the search bar. This is the final code snippet for this tutorial.

import React, { useState, useEffect } from 'react';
import { View, Text, FlatList } from 'react-native';
import { SearchBar } from 'react-native-elements';
import { countries } from 'countries-list';

const useDebounce = (value: any, delay: number) => {
  const [debounceValue, setDebounceValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounceValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debounceValue;
};

// Get the values of the countries and sort is alphabetically
const countryList = Object.values(countries)
  .map(country => ({
    ...country,
    lowerCaseName: country.name.toLowerCase(),
  }))
  .sort((a, b) => a.name > b.name);

const SearchableFlatList = () => {
  const [query, setQuery] = useState('');
  const debounceQuery = useDebounce(query, 300);
  const [filteredCountryList, setFilteredCountryList] = useState(countryList);

  useEffect(() => {
    const lowerCaseQuery = debounceQuery.toLowerCase();
    const newCountries = countryList
      .filter((country) => country.lowerCaseName.includes(lowerCaseQuery))
      .map((country) => ({
        ...country,
        rank: country.lowerCaseName.indexOf(lowerCaseQuery),
      }))
      .sort((a, b) => a.rank - b.rank);

    setFilteredCountryList(newCountries);
  }, [debounceQuery]);

  return (
    <View>
      <SearchBar
        placeholder="Search your countries..."
        onChangeText={setQuery}
        value={query}
      />
      <FlatList
        keyExtractor={(item, index) => `${index}`}
        data={filteredCountryList}
        renderItem={({item}) => <Text>{item.name}</Text>}
      />
    </View>
  );
};

export default SearchableFlatList;

Congratulations! We successfully build a searchable FlatList in React Native.

Hope you like the tutorial and able to build a performant search in React Native. I believe there are many ways to achieve this. If you found any better solution, please feel free to share it in the comment below!

Supercharge React Native Android app with Hermes

Facebook officially released the Hermes engine as an opt-in feature on Android. By enabling Hermes, Facebook claims that it will improve startup time, decrease memory usage and reduce app size. Let’s find out!

Prerequisite

To enable Hermes, your app needs to be at least using React Native 0.60.4 and above. If you are using the earlier version of React Native, you may need to upgrade it with React Native Upgrade Helper.

Enable Hermes

Just a few simple steps that are required to enable Hermes in React Native.

Find the line of enableHermes: false in your app/build.gradle and change it to true.

project.ext.react = [
     entryFile: "index.js",
     enableHermes: true  // clean and rebuild if changing
]

If you are using proguard, add the following line in your proguard-rules.pro

-keep class com.facebook.hermes.unicode.** { *; }

You need to clean your project if you have built your app before.

$ cd android && ./gradlew clean
$ react-native run-android

Voila! Your Android app is now using Hermes engine.

Confirming Hermes is in Use

To verify if Hermes has been enabled, you can always verify it with the following code.

const isHermes = () => global.HermesInternal != null;

Analysing the result

Now, let’s take a look at how much improvement that Hermes can do. I have enabled Hermes in one of my React Native app and let’s look at the result

Spendie – An Expenses Tracking App

Before HermesAfter HermesImprovement
App Size (APK)26.4MB16.8MB36% size reduced
Spendie (with Hermes enabled) vs Spendie (without Hermes enabled)

Common Issue about Hermes

#1 Build error in CI/CD environment

If you are using React Native 0.60.x, you may hit compilation error in your CI/CD environment such as TravisCI, CircleCI or even Github Actions.

Process 'command '../../node_modules/hermesvm/linux64-bin/hermes'' finished with non-zero exit value 135

This issue should be fixed in latest release of React Native 0.61.x. However, if you are not able to upgrade React Native version. You may apply the following fixes.

First, you need to install hermes-engine.

yarn add -D hermes-engine

Update the hermesCommand to use the hermes-engine in your app/build.gradle

project.ext.react = [
    entryFile: "index.js",
    enableHermes: true,
    hermesCommand: "../../node_modules/hermes-engine/%OS-BIN%/hermes", // Add this line
]

Change the hermesvm to hermes-engine in app/build.gradle

// Find this line
def hermesPath = "../../node_modules/hermesvm/android/";

// Change to the following
def hermesPath = "../../node_modules/hermes-engine/android/";
#2 Blank screen when upload to Play Store with bundle

Many issues have been reported that if you bundle your app and upload to Play Store, your app will show blank screen when start. To temporary fix this problem, you may use assemble instead of bundle at the moment.

Although you may faced some problem with Hermes engine but please feel free to enable Hermes for your React Native app and enjoy the performance boost!

To understand more about Hermes engine, you may read the Facebook post.

React Native Continuous Delivery with Github Actions and Fastlane

Github Actions is the workflow automation tool with CI/CD that allows you to do some tasks, such as running test suite, deploying code and etc based on the Github Events and Types. When an event is triggered, your defined workflow will be run and help you to do some awesome jobs.

Today, I’m going to show you how to create the workflow of building your React Native apps and publishing them to App Store Connect and Play Store using Github Actions and Fastlane. By end of this post, we will be able to build and publish iOS and Android app to respective store when a new Github Release is published. Sounds exciting? Let’s get started!

Creating Workflow

First, we will have to create a workflow in .github/workflows directory. Similar to other CI/CD services, you may configure the workflow using YAML syntax. Multiple workflow files can be created in the directory and each workflow must have at least a Job.

Now, let’s create a publish.yml workflow and put a name for the workflow.

name: Publish iOS and Android App to App Store and Play Store

Setting Trigger Event

We want to trigger the workflow when a Github Release is published. Thus, we will be using the release event in Github Actions to trigger our workflow. Besides release event, Github Actions also contains many events such pull_request, push, issues that you can hook on and run the workflow. Many events such as release, pull_request may have more than one type of activities. For example, the release event will be triggered when a release is published, created, unpublished, edited and etc.

In this post, we want to trigger the workflow when the event is release and the activity type is published.

name: Publish React Native App to App Store and Play Store

+ on:
+  release:
+    type: [published]

If you want to trigger event with more than one activity type, you may add more activity type into the array. Other than web hook events, workflows can be triggered by scheduled job and external events too.

To understand more about triggering events, please read Events that trigger workflow

Creating Jobs

As mentioned above, each workflow must have at least a Job. Since we are building iOS and Android app, let’s add two jobs: release-ios and release-android in the workflow.

name: Publish React Native App to App Store and Play Store

on:
  release:
    type: [published]

+jobs:
+  release-ios:
+    name: Build and release iOS app
+    runs-on: macOS-latest
+
+  release-android:
+    name: Build and release Android app
+    runs-on: ubuntu-latest

By default, every job will be started simultaneously. If you want to start a job after another job is completed, you can use needs to specify the dependencies of a job.

jobs:
  job1:
  job2:
    needs: job1
  job3:
    needs: [job1, job2]

In this example, job2 will starts after job1 is completed and job3 will starts after job1 and job2 is completed.

For every job, we will also need to specify which OS that the job should runs on, Github Actions provides three essential OS which is windows, ubuntu and macOS. In this post, we will use macOS-latest for iOS build and ubuntu-latest for Android build

To understand more about the virtual environment, please read Virtual environment for Github Actions

Defining Steps

Now, we can start to add the steps of building and releasing the iOS and Android app. Github Actions provides several standard actions for us to perform certain tasks such as checkout the repository, installing Node, installing Ruby and etc. You can create your own actions if you want to. We will use few standard actions such as actions/checkout, actions/setup-node and actions/setup-ruby to setup the environment.

name: Publish React Native App to App Store and Play Store

on:
  release:
    type: [published]

jobs:
  release-ios:
    name: Build and release iOS app
    runs-on: macOS-latest
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions/setup-node@v1
+        with:
+          node-version: '10.x'
+      - uses: actions/setup-ruby@v1
+        with:
+          ruby-version: '2.x'
+      - name: Install Fastlane
+        run: bundle install
+      - name: Install packages
+        run: yarn install

  release-android:
    name: Build and release Android app
    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions/setup-node@v1
+        with:
+          node-version: '10.x'
+      - uses: actions/setup-ruby@v1
+        with:
+          ruby-version: '2.x'
+      - name: Install Fastlane
+        run: bundle install
+      - name: Install packages
+        run: yarn install

In the workflow above, we have added few steps as following:

  1. actions/checkout@v1 – Checkout the current repository.
  2. actions/setup-node@v1 – Install Node 10.x to run React Native >= 0.60
  3. actions/setup-ruby@v1 – Install Ruby 2.x for the usage of Fastlane
  4. bundle install – Install Fastlane
  5. yarn install – Install NPM packages

Now, we have all the basic setup in place and we will continue to add more steps to build iOS and Android app using Fastlane. Let’s start with building and publishing Android app.

Build and Publish Android app

There are 2 things that we need to build and publish Android app:

  1. keystore – Signing the APK. Learn more about how to create your keystore.
  2. Google Credentials – Authenticate with Play Console for publishing the app. Learn more about how to create your Google Credential.

After you have created the keystore and the Google Credential, you may want to encrypt them using command gpg --symmetric --cipher-algo AES256 path/to/your-secret.json and commit to your repository.

Now, let’s create a script to decrypt the keystore and the Google Credential so that we can use them in our workflow. Create decrypt.sh and add the following codes:

#!/bin/sh

# --batch to prevent interactive command --yes to assume "yes" for questions
gpg --quiet --batch --yes --decrypt --passphrase="$ENCRYPT_PASSWORD" \
--output ./path/to/release.keystore ./path/to/release.keystore.gpg

gpg --quiet --batch --yes --decrypt --passphrase="$ENCRYPT_PASSWORD" \
--output ./path/to/google-key.json ./path/to/google-key.json.gpg

The ENCRYPT_PASSWORD is the password that you used to encrypt your secret files and we will put it as a environment variable later. Now let’s add the remaining steps to complete the Android workflow.

name: Publish React Native App to App Store and Play Store

on:
  release:
    type: [published]

jobs:
  release-ios:
    ...

  release-android:
    name: Build and release Android app
    runs-on: ubuntu-latest
    steps:
      ...
+      - name: Decrypt keystore and Google Credential
+        run: ./path/to/decrypt.sh
+        env:
+          ENCRYPT_PASSWORD: ${{ secrets.ENCRYPT_PASSWORD }}
+      - name: Bundle and Upload to PlayStore
+        run: bundle exec fastlane build_and_release_to_play_store versionName:${{ github.event.release.tag_name }}
+        env:
+          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
+          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

We are using the name of the tag as the versionName of the app. To add environment variables in Github Actions, we can add env in the steps that need the variables. If you want to add any secrets, you may add the secrets to your Github repository’s Settings and access them using ${{ secrets.YOUR_SECRET_NAME }}.

We are almost there. Let’s add the build_and_release_to_play_store action in the Fastfile.

lane :build_and_release_to_play_store do |options|
  # Bundle the app
  gradle(
    task: 'bundle',
    build_type: 'Release',
    project_dir: "android/",
    properties: {
      "versionName" => options[:versionName],
      "versionCode" => options[:versionCode],
    }
  )

  # Upload to Play Store's Internal Testing
  upload_to_play_store(
    package_name: 'com.example.app',
    track: "internal",
    json_key: "./path/to/google-key.json",
    aab: "./android/app/build/outputs/bundle/release/app.aab"
  )
end

Awesome! We have completed the Android steps and you should able to build and publish the app to Play Store.

Building and Publish iOS app

To build an iOS app, we will need to sign the IPA before upload it to App Store Connect and there is no easy way of doing it in CI/CD environment. Luckily, Fastlane provides the sync_code_signing action for us to handle the code signing easily. If you have not setup code signing before, please follow the codesigning guideline to generate your certificates and provisioning profiles.

Once you done setup the code signing, you can add the steps to the workflow

name: Publish React Native App to App Store and Play Store

on:
  release:
    type: [published]

jobs:
  release-ios:
    name: Build and release iOS app
    runs-on: macOS-latest
    steps:
      ...
      - name: Login Github User
        run: echo -e "machine github.com\n  login $PERSONAL_ACCESS_TOKEN" >> ~/.netrc
        env:
          PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
      - name: Build and Upload to TestFlight
        run: bundle exec fastlane build_and_release_to_app_store versionName:${{ github.event.release.tag_name }} versionCode:${{ github.run_number }}
        env:
          FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}

  release-android:
    ...

In Github Actions, we can only access the current repository that the workflow is running. If you want to checkout other private repositories, you may create a <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line&quot; target="_blank">personal access token</a> and login using netrc echo -e "machine github.com\n login $PERSONAL_ACCESS_TOKEN" >> ~/.netrc. We have added this step to allow us to checkout the code signing repository.

In addition, we will need to add FASTLANE_PASSWORD and MATCH_PASSWORD in the environment variables so that Fastlane able to decrypt the certificates and provisioning profiles as well as authenticate with App Store Connect.

If your Apple account has activated two-factor authentication, you may want to add FASTLANE_SESSION and FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD into the environment variables, otherwise the workflow will fail. Read Fastlane two-factor or two-steps auth to learn more.

Now let’s add build_and_release_to_app_store actions into Fastfile.

lane :buid_and_release_to_play_store do |options|
  ...
end

lane :build_and_release_to_app_store do |options|
  # Set the build number
  increment_build_number(
    build_number: options[:versionCode],
    xcodeproj: "./ios/Example.xcodeproj"
  )

  # Set the version name
  increment_version_number(
    version_number: options[:versionName],
    xcodeproj: "./ios/Example.xcodeproj"
  )

  # Create a custom keychain for code signing
  create_keychain(
    name: 'keychain',
    password: 'password',
    default_keychain: true,
    unlock: true,
    timeout: 3600,
    add_to_search_list: true
  )

  # Import the appstore code signing
  match(
    type: "appstore",
    keychain_name: 'keychain',
    keychain_password: 'password',
    readonly: true
  )

  # Building the iOS app
  gym(
    workspace: "./ios/Example.xcworkspace",
    include_bitcode: true,
    include_symbols: true,
    silent: true,
    clean: true,
    scheme: "Example",
    export_method: "app-store"
  )

  # Upload to testflight
  testflight(
    app_identifier: "com.example.app",
    username: "your_apple_username@mail.com",
    skip_submission: true,
    skip_waiting_for_build_processing: true
  )
end

Awesome! We have completed the iOS job! Now you should able to build and publish your iOS app to App Store Connect now.

Testing Your Workflow

To test your workflow, you can create a Release and go to the Actions tab in Github to view the log of your workflow.

Github Actions is here for you!

Github Actions is a very awesome tool for every developer to start with CI/CD journey. To understand more about Github Actions, please read their documentation. Have fun trying out Github Actions 😊 !

Testing React Native With Jest, react-test-renderer And react-native-testing-library

React Native is a great framework for you to write once and build mobile app with Javascript and React to both Android and iOS platform. Everything is pretty easy until your superior telling you, “We need to start practice TDD approach because there are too many bugs surfaced in production to capture the error early”. Sounds familiar? 

Okay, today we will look into how to test React Native components using Jest and react-test-renderer as well as react-native-testing-library. There are few common scenarios that you will need to test for a component and we will cover them in this post.

  • Snapshot testing
  • Event triggering
  • Async API call
  • Timer event

In this post, all the components will be using React Hooks

Testing component with Snapshot testing

Snapshot testing allow us to easily verify if the component​ UI is work as expected and not changing unexpectedly. Jest will capture a snapshot of the component when the first time you run the test and verify the result against the snapshot for the subsequence test run. If the result is not same as snapshot, it will fail the tests.

Now let’s try it with HelloWorldComponent below.

import React from 'react';
import { Text } from 'react-native';

const HelloWorldComponent = () => {
  return (
    <Text>Hello World</Text>
  );
};

export default HelloWorldComponent;

And here is the test looks like

import React from 'react';
import renderer from 'react-test-renderer';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should display Hello World', () => {
    // Render a React component
    const component = renderer.create(<HelloWorldComponent />);

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

Your snapshot taken will be look like this.

exports[`HelloWorldComponent should display Hello World 1`] = `
<Text>
  Hello World
</Text>
`;

It is very hard to do snapshot testing right as when your components is complex, you will have a hard time to check on the snapshot (With your eyes i mean). Here is some advice from me: Iterate the component piece by piece and commit your changes to source control so that you can verify the changes for each iteration is correct.

Testing Event Trigger

Another scenario that we commonly need to test is event triggering, such as button click. Let’s add a Button in the HelloWorldComponent.

import React, { useState } from 'react';
import { Button, View, Text } from 'react-native';

const HelloWorldComponent = () => {
  const [text, setText] = useState('Hello World');

  const onButtonPress = () => {
    setText('I have pressed the button.');
  };

  return (
    <View>
      <Button testID="button" onPress={onButtonPress} title="Change text" />
      <Text>{text}</Text>
    </View>
  );
};

export default HelloWorldComponent;

We added a Button that will change the text to “I have pressed the button” after it is pressed. You will realize that we have added “testID” props for the Button, this props will be used to look up the Button and trigger the onPress() event during the test. react-test-render provide few methods for you to look up the descendent component, you are free to use any of those look up methods.

​Now, let’s test the onPress event.

import React from 'react';
import renderer, { act } from 'react-test-renderer';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should change text after press button', () => {
    // Render a React component
    const component = renderer.create(<HelloWorldComponent />);

    // Find the button that has props.testID === 'button'
    const button = component.root.findByProps({ testID: 'button' });

    // All codes that causes state updates should wrap in act(...)
    act(() => {
      // Call the onPress props of the button
      button.props.onPress();
    });

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

You should see the text is displaying as “I have pressed the button.”. In this test, we use “act” to wrap the button press event trigger because all events that causes the state updates should wrap in “act”, otherwise, the changes will not reflect to the component.

Testing Asynchronous Event

Another very common scenario is that we will retrieve some data from server and display it when we enter a page. Let’s change our HelloWorldComponent to become that scenario now.

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Text } from 'react-native';

const HelloWorldComponent = () => {
  const [text, setText] = useState('Hello World');

  useEffect(() => {
    const getTextFromServer = async () => {
      // Get response from server
      const response = await axios.get('https://example.com');

      // Set text
      setText(response.data);
    };

    getTextFromServer();
  }, []);

  return (
    <Text>{text}</Text>
  );
};

export default HelloWorldComponent;

We have left the dependencies of “useEffect” as empty so that it’s only trigger once, which similar to “componentDidMount”. Now let’s try to write the test.

import React from 'react';
import renderer from 'react-test-renderer';
import axios from 'axios';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should display text from server', async () => {
    // Mock response from axios
    jest.spyOn(axios, 'get').mockReturnValue({ data: 'This is server response' });

    // Render a React component
    const component = renderer.create(<HelloWorldComponent />);

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

It’s pretty simple, isn’t it? We just need to mock the response for “axios” and the test should be complete. Let’s run the test and wait for green light! ………..Erm……let us check on the snapshot….oh no…the text is still remaining as “Hello World”. What is wrong here? Don’t worry, this is working as expected…If you are familiar with “useEffect”, you will know it will run after the component layout and painted. You can read more for the timing of effect docs. There are thorough discussion happening in this issue about how to solve this problem but today I wanted to solve this problem using utils called react-native-testing-library which highly inspired by react-testing-library. This library provided some useful function to re-render component or clear all async tasks. Let’s make some changes to our test.

import React from 'react';
import { render, flushMicrotasksQueue } from 'react-native-testing-library';
import axios from 'axios';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should display text from server', async () => {
    // Mock response from axios
    jest.spyOn(axios, 'get').mockReturnValue({ data: 'This is server response' });

    // Render a React component
    const component = render(<HelloWorldComponent />);
    
    // Flush all tasks queued
    await flushMicrotasksQueue();

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

There are 2 things we changed here:
1. We are no longer directly using react-test-renderer to render the component, instead we use { render } from react-native-testing-library.
2. We added “await flushMicrotasksQueue()” to flush all the queued tasks in JS.
Now, we can re-run the test and we should see the display text as “This is server response”.

Testing Timer Event

Last thing we need to look into today is testing timer event, there are also occasion that we will need to setTimeout to trigger some changes or setInterval to repeatedly trigger some polling. Let’s add some time out event in our HelloWorldComponent.

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';

const HelloWorldComponent = () => {
  const [text, setText] = useState('Hello World');

  useEffect(() => {
    const timer = setTimeout(() => {
      setText('Timer is done!');
    }, 3000);

    return () => {
      clearTimeout(timer);
    };
  }, []);

  return (
    <Text>{text}</Text>
  );
};

export default HelloWorldComponent;

Now, HelloWorldComponent will change the text to “Timer is done!” after 3 seconds, but how can we test it after 3 seconds? Are we going to wait 3 seconds in the test? Of course no! We can use “jest.useFakeTimers()” to mock the timer and run them immediately. Here is how it’s looks for the test:

import React from 'react';
import { render } from 'react-native-testing-library';

import HelloWorldComponent from '../HelloWorldComponent';

describe('HelloWorldComponent', () => {
  test('should change text after 3 seconds', async () => {
    // Fake timer
    jest.useFakeTimers();

    // Render a React component
    const component = render(<HelloWorldComponent />);

    // Tell jest to run remaining timer and not create any new timer
    jest.runOnlyPendingTimers();

    // Expect the result
    expect(component.toJSON()).toMatchSnapshot();
  });
});

All we need to do is to fake the timer and tell jest to run the timer, then we got the result we wanted!

For this post, we have looked in to how to test for component using snapshot, event triggering, async event as well as timer event using Jest, react-test-renderer or react-native-testing-library. If you would like to use react-native-testing-library to test all the scenarios, you are free to do so too.

Thank you for reading!

Shareable ESLint Config For React Native

A wise programmer once said, “You don’t write code for yourself, you write for others or your future-self”. Imagine that you are working on a project which everyone is writing in their own style and standard. Everyday, you need to spend some times to get familiar with the code before you can start coding. It’s very inefficient for everyone’s daily works. Thus, code consistency and readability is very important for every project. Most of the programming language have their own linter, and for Javascript, Eslint is one of the best.

If you take a look at this repo, awesome-eslint, and you will notice there are tons of plugin and rules for Eslint. You may wonder what is the Eslint plugins or configs that you can use for your React Native project. Today, I’m going to share with you some basic Eslint plugins that you can have for your React Native project.

Standard Eslint for React Native

Airbnb Javascript Style Guide​
One of the most adopted Javascript Eslint config because it has the most reasonable style for Javascript programming. In this config, it’s also included linting for React and ECMAScript 6+. If you are not doing for React, Airbnb does publish another config that is without React, eslint-config-airbnb-base.

Eslint Plugin for React Native
On top of React, this plugin provide extra few linting particularly for React Native, such as no-unused-stylesno-inline-stylessplit-platform-components.

EslintPlugin for React Hooks
In React 16.8, Facebook introduce React Hooks, where you can use state and other features of React without a Class. It’s also allow sharing of state logic across components. Together with React Hooks, Facebook also introduce this Eslint plugin to enforce the rules of Hooks. If you plan to use Hooks, you are highly recommended to install this plugin.

Eslint Plugin for Jest
Jest is a testing framework that widely use for React and React Native applications, it’s easy to setup and most importantly, it has snapshot assertion that allow you to test for large object, for example, your React component. This plugin basically will help to guide and lint your Jest tests.

Eslint Plugin for Flowtype
Flowtype is the static type check for Javascript, it’s allow you to run checking of your code for any type error and missing props. In this plugin, it’s basically check for all the naming convention of type, duplication of type, missing type and etc.

That is all the basic Eslint plugins and configs for React Native, but what if you are working with many React Native projects and you want to adopt these Eslint plugins into all the projects? You will need to install all of them one by one and rewrite all the rules again? I beg you will get yourself crazy with that….

Create Shareable Eslint Config

To avoid duplicate work on settings up Eslint in your projects, you can create a shareable config that allow you to keep all your projects using the same Eslint configuration. First, let’s start with create a new Node.js module by create a folder and run npm init, remember to name the package with eslint-config-<your config name> as that is the standard for Eslint shareable config.

Next, create a index.js and put all your rules in the file, for example:

module.exports = {
  extends: [
    'airbnb',
  ],
  plugins: [
    'react',
  ],
};

To test your config before publish, you can run npm link in your shareable config folder, and run npm link eslint-config-<your config name> in the project that you want to use the config. Then, you need to update your project’s eslintrc.js by extending the shareable config.

{
    "extends": "eslint-config-<your config name>"
}

Now you should able to test your shareable config in your local machine. Before you want to publish your shareable config, you need to add the dependencies in package.json, at least, you need to define the minimum Eslint version you depending on.

"peerDependencies": {
    "eslint": ">= 3"
}

Now, you can run npm publish and your shareable config are now ready to use! In the end of the reading, I hope it’s give you some clues on how to create a shareable config for your project or organisation.

Lastly, I have created the shareable Eslint config for React Native, eslint-config-react-native-standard, please feel free to use or contribute. Thank you for reading!