Localhost

React Native Expense Manager

Example

Through this succint tutorial, we will go through the design and development of a small expense manager in React Native. This app will help a user save and tag expenses on a remote server using a REST API. It will need to store data locally when no Internet connection is available and sync it when a connection is detected.

The following is not intended to be a full tutorial, but rather a compilation of notes and troubleshooting tips. This tutorial only covers the initial setup steps and not all the business logic of handling an expenses

Design and UI

The app will have 3 main screens:

  • SplashScreen
  • MainScreen containing a list of all the expenses
  • AddExpenseScreen where the user can input the amount, category of the expense

Setup Environment

For this app, I am going to use React Native without Expo.

You can see why not to use Expo here

Set ANDROID_HOME variable

You will need to make the ANDROID_HOME variable available. You can do this by setting it in the end of ~/.bashrc or ~/.bash_profile files:

sudo nano ~/.bashrc
export ANDROID_HOME=/path/to/Android/Sdk
export PATH=${PATH}:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools
export JAVA_HOME=/usr/lib/jvm/java-8-oracle

Install dependencies using yarn

If you don't have yarn installed, head to this page. I do not recommend using npm commands alongside yarn's.

$ yarn add global react-native 
$ yarn add global react-native-cli
$ npm install -g react-native-cli

Now use the cli to scaffold the app skeleton:

$ react-native init ExpenseManager

This will generate the following structure: Example

Run the app

Now test everything by running:

$ react-native run-android

You will need to run an emulator or plugin a device beforehand. Example


App Architecture

Requirement 1: Persistence

https://github.com/rt2zz/redux-persist

Requirement 2: Offline usage

https://github.com/redux-offline/redux-offline


Create the UI

First, we are going to create the three screens with dummy data.

Create a folder called components at the root of the app's folder and under components/screens create three files: SplashScreen.js, MainScreen.js and AddExpenseScreen

In order to navigate between all different screens, I am going to use React Native Navigation.

Install it:

yarn add react-native-navigation --save

Along with React Native Navigation, we will be using React Native Redux which will offer us the possibility to manage an app wide store state of the app. Install React Native Redux:

yarn add redux react-redux redux-logger react-native-gesture-handler --save

redux-logger is quite handy to see the content of the store state Setup Navigation

Create a new file name Navigator.js in a Navigation folder:

import React from 'react'
import SplashScreen from '../components/screens/SplashScreen'
import MainScreen from '../components/screens/MainScreen'
import AddExpenseScreen from '../components/screens/AddExpenseScreen'
import { createStackNavigator, 
  createAppContainer,
 } from 'react-navigation'

const RootStack = createStackNavigator({
  SplashScreen: { screen: SplashScreen },
  MainScreen: { screen: MainScreen },
  AddExpenseScreen: { screen: AddExpenseScreen },
},{
  initialRouteName: 'SplashScreen',
  headerMode: 'none'
})

const App = createAppContainer(RootStack)

export default App

Setup the reducer

Add a new file caller Reducer.js at the app's root folder:

import { Navigator } from './Navigation/Navigator'
import { NavigationActions } from 'react-navigation'

const initialAction = { type: NavigationActions.Init }
const initialState = { 
    expenses: [], 
    isLoading: true
};


export default (state = initialState, action) => {
    return { ...state }
}

We will handle persisting the store state later. Now, import the newly created reducer in the App.js file:

import React, { Component } from 'react';
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import logger from 'redux-logger'
import Reducer from './Reducer'
import Navigator from './Navigation/Navigator'

const reducer = combineReducers({ Reducer })
const store = createStore(reducer, applyMiddleware(logger))

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Navigator />
      </Provider>
    );
  }
}

Create SplashScreen

// SplashScreen.js

import React, { Component } from 'react'
import { StyleSheet, View, Text } from 'react-native'
import Colors from '../../utils/Colors'

export default class SplashScreen extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.title}>Expense Manager</Text>
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex:1,
    backgroundColor: Colors.background,
    justifyContent: 'center',
    alignItems: 'center'
  },
  title: {
    position: 'absolute',
    fontFamily: 'Roboto',
    fontStyle: 'normal',
    fontWeight: 'bold',
    fontSize: 48,
    lineHeight: 45,
    alignSelf: 'center',
    letterSpacing: -0.02,
    color: '#FFFFFF'
  }
})

The Splash Screen contains a simple text and no significant code. Here is the result: This image has an empty alt attribute; its file name is image-7-576x1024.png

Now add the logic to transition from SplashScreen to the MainScreen. We will just use a timeout. Add this to the SplashScreen component:

...
  componentDidMount(){
    setTimeout(()=>{
      this.props.navigation.navigate('MainScreen')
    }, 5000);
  }
...

Create MainScreen

The main screen is comprised of 3 components:

  • Header
  • AddExpense Button
  • List of all Expenses

We are going to use the components from React Native Elements. Add the dependencies:

$ yarn add react-native-elements --save
$ yarn add react-native-vector-icons --save
$ yarn add react-native-config --save
$ react-native link react-native-config
$ react-native link react-native-vector-icons

Checkout this excellent tutorial on how to use flex for designing the UI.

Create AddExpenseScreen

This screen has the following sections:

  • A header with two buttons
  • An input text for the expense amount
  • A list to select the expense category
  • A date picker
  • A text input for expense comment

Add the following dependencies:

$ yarn add react-native-sectioned-multi-select --save
$ yarn add react-native-datepicker --save

react-native-sectioned-multi-select is a handy select component which we will use to let the user see a predefined categories list. This list is the following:

export default categories = [
  {
    name: "Food",
    id: 0,
    children: [
      {
        name: "Food",
        id: 10,
      },
      {
        name: "Groceries",
        id: 20,
      },
      {
        name: "Other groceries",
        id: 30,
      }
    ]
  },
  {
    name: "Utility services",
    id: 1,
    children: [
      {
        name: "Heating",
        id: 40,
      },
      {
        name: "Security",
        id: 50,
      },
      {
        name: "Electricity",
        id: 60,
      },
      {
        name: "Natural gas",
        id: 70,
      },
      {
        name: "Rent",
        id: 80,
      },
      {
        name: "Telephone",
        id: 90,
      },
      {
        name: "Internet",
        id: 100,
      },
      {
        name: "Water",
        id: 110,
      },
      {
        name: "Other utility expenses",
        id: 120,
      }
    ]
  },
  {
    name: "Household",
    id: 2,
    children: [
      {
        name: "Home, construction, garden",
        id: 130,
      },
      {
        name: "Household goods",
        id: 140,
      },
      {
        name: "Household products and electronics",
        id: 150,
      }
    ]
  },
  {
    name: "Transportation",
    id: 3,
    children: [
      {
        name: "Parking",
        id: 160,
      },
      {
        name: "Fuel",
        id: 170,
      },
      {
        name: "Transportation expenses",
        id: 180,
      },
      {
        name: "Vehicle purchase, maintenance",
        id: 190,
      }
    ]
  },
  {
    name: "Clothing",
    id: 4,
    children: [
      {
        name: "Shoes",
        id: 200,
      },
      {
        name: "Clothing",
        id: 210,
      },
      {
        name: "Other clothing expenses",
        id: 220,
      }
    ]
  },
  {
    name: "Leisure activities, traveling",
    id: 5,
    children: [
      {
        name: "Charity",
        id: 230,
      },
      {
        name: "Gifts",
        id: 240,
      },
      {
        name: "Books, newspapers, magazines",
        id: 250,
      },
      {
        name: "Pets",
        id: 260,
      },
      {
        name: "Accommodation, travel expenses",
        id: 270,
      },
      {
        name: "Sport and sports goods",
        id: 280,
      },
      {
        name: "Theatre, music, cinema",
        id: 290,
      },
      {
        name: "Hobbies and other leisure time activities",
        id: 300,
      },
      {
        name: "Nightlife",
        id: 310,
      }
    ]
  },
  {
    name: "Education, health and beauty",
    id: 6,
    children: [
      {
        name: "Education and courses",
        id: 320
      },
      {
        name: "Beauty, cosmetics",
        id: 330
      },
      {
        name: "Social security",
        id: 340
      },
      {
        name: "Health and pharmaceuticals",
        id: 350
      },
      {
        name: "Other expenses for education, health, beauty",
        id: 360
      }
    ]
  },
  {
    name: "Children",
    id: 7,
    children: [
      {
        name: "Children's clothing",
        id: 370
      },
      {
        name: "Hobbies and toys",
        id: 380
      },
      {
        name: "Pocket money",
        id: 390
      },
      {
        name: "School and baby-sitting",
        id: 400
      },
      {
        name: "Other child-related expenses",
        id: 410
      }
    ]
  },
  {
    name: "Insurance",
    id: 8,
    children: [
      {
        name: "Life insurance",
        id: 420
      },
      {
        name: "Car insurance",
        id: 430
      },
      {
        name: "Home insurance",
        id: 440
      },
      {
        name: "Loan insurance",
        id: 450
      },
    ]
  },
  {
    name: "Loans and financial services",
    id: 9,
    children: [
      {
        name: "Financial services and commission",
        id: 460
      },
      {
        name: "Fines",
        id: 470
      },
      {
        name: "Loans",
        id: 480
      },
      {
        name: "Payday loans",
        id: 490
      },
      {
        name: "Consumer loans",
        id: 500
      },
      {
        name: "Leasing",
        id: 510
      },
      {
        name: "Car Leasing",
        id: 520
      },
      {
        name: "Mortgage",
        id: 530
      },
      {
        name: "Credit card repayment",
        id: 540
      },
      {
        name: "Credit line",
        id: 550
      },
      {
        name: "Student loans",
        id: 560
      },
      {
        name: "Overdraft",
        id: 570
      }
    ]
  },
  {
    name: "Other",
    id: 5,
    children: [
      {
        name: "Other",
        id: 580
      }
    ]
  }
]

The selector is very easy to use:

...
          <SectionedMultiSelect
            items={dataList}
            uniqueKey='id'
            single={true}
            showChips={false}
            subKey='children'
            selectText='Choose category...'
            showDropDowns={true}
            readOnlyHeadings={true}
            onSelectedItemsChange={this.onSelectedItemsChange}
            selectedItems={this.state.expense.category}
          />
...

Similarly the date picker is used like this:

...
<DatePicker
            date={this.state.expense.date}
            mode="date"
            placeholder="select date"
            format="DD-MM-YYYY"
            minDate="01-01-2019"
            maxDate="01-01-2035"
            confirmBtnText="Confirm"
            cancelBtnText="Cancel"
            onDateChange={this.onDateChange}
          />
...

The screen will look like this. Example

It is a little bit different from the first design but I have found this card based layout a little bit easier to use. This image has an empty alt attribute; its file name is image-6-576x1024.png

Now that the visual components are done, head to part 2 of this tutorial to see how we fetch data from the API.


Part 2: Using Redux and Redux SAGAS

Redux is a very important library to manage the app's state. This state will be accessible across all our app's components without the need to pass the state to each component through props

In our App.js we create the app's store, to which we pass the initial state

const rootReducer = combineReducers({ Reducer })
const store = createStore(rootReducer, applyMiddleware(logger))

export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Navigator />
      </Provider>
    );
  }
}

The rootReducer initiates the state:

const initialState = {
    expenses: [],
    isLoading: true
}

export default (state = initialState, action) => {
    return { ...state }
}

You should see the reducer as the way to edit the state through sending signals to the store. These signals are referred to as actions. Each action must have a type.

We will declare the following actions:

// App/utils/Constants.js

// Action types
export const ADD_EXPENSE = "ADD_EXPENSE"
export const EDIT_EXPENSE = "EDIT_EXPENSE"
export const DELETE_EXPENSE = "DELETE_EXPENSE"
export const FETCH_EXPENSES = "FETCH_EXPENSES"
export const EXPENSES_LOADED = "EXPENSES_LOADED"
export const EXPENSES_LOAD_FAILED = "EXPENSES_LOAD_FAILED"

The state is immutable. You updated it by copying the current state's data and adding in new data.

Create a new file in App/actions. let's name it index.js. This will hold all the dispatch action code for our 4 actions above. Comes Redux SAGA. Redux Saga is a middleware that comes in handy to manage the asynchronous API calls. Setup the middleware in App.js like this:

import React, { Component } from 'react';
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import { createSagaMiddleware } from 'redux-saga'
import logger from 'redux-logger'
import Reducer from './reducers/Reducer'
import Navigator from './Navigation/Navigator'

const sagaMiddleware = createSagaMiddleware()
const rootReducer = combineReducers({ Reducer })
const store = createStore(
  rootReducer, 
  applyMiddleware(sagaMiddleware, logger),
)

sagaMiddleware.run(rootSaga)

...

Manage REST API calls

Axios is a useful library to make HTTP calls. We will use it alongside redux-saga to make our network requests.

Add the dependency:

$ yarn add axios --save

Then import it in our saga file:

import { takeLatest, call, put } from "redux-saga/effects";
import axios from "axios";

// axios network requests

// function that makes the api request and returns a Promise for response
function fetchExpenses() {
  return axios({
    method: "get",
    url: "https://dog.ceo/api/breeds/image/random"
  });
}

...

Set API token

Usually you will access secure APIs through some authorization mechanism. In our example API, we will need an auth token. It is recommended to put it in an env variable and access it using react-native-dotenv. First, update your babel presets to add this package:

module.exports = {
  presets: ['module:metro-react-native-babel-preset', 'react-native-dotenv'],
};

Create a file called .env at the root of the project:

API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx

You can access this value anywhere in your code by importing it:

import { API_TOKEN } from 'react-native-dotenv'

Add authorization header to API calls

...
import { API_TOKEN } from 'react-native-dotenv'
...

let config = {
    headers: {
      'Authorization': 'Bearer ' + API_TOKEN
    }
}

// axios network requests
function fetchExpenses() {
    return axios.get(API_URL+API_URL_EXPENSES, config)
}
...

Using generator functions

Example: generator functions

A generator is a function that can stop midway and then continue from where it stopped.

source

function* fetchData() {
    try {
      const response = yield call(fetchExpenses);
      const expenses = response.data.expenses;

      // dispatch a success action to the store with the new data
      yield put({ type: EXPENSES_LOADED, expenses });

    } catch (error) {
      // dispatch a failure action to the store with the error
      yield put({ type: EXPENSES_LOAD_FAILED, error });
    }
}

function* dataSaga () {
  yield takeLatest(FETCH_EXPENSES, fetchData)
}

If you are using React Navigation version 3+, it is recommended you don't try to make redux manage the nav state and leave to RNN to do that.

Can I store the navigation state in Redux too? This is technically possible, but we don't recommend it - it's too easy to shoot yourself in the foot and slow down / break your app. We encourage you to leave it up to React Navigation to manage the navigation state. But if you really want to do this, you can use react-navigation-redux-helpers, but this isn't an officially supported workflow -- from RNN docs

Testing Sagas

For a start, head to this excellent article about testing redux sagas

Head to part 3, to see how to connect our containers to the Redux store.


Part 3: connect our containers to the Redux Store

We will user Redux's connect() in our containers (screens) in order to make them connected to the Redux Store.

Example: show a loader when expenses are being fetched from the server, show an empty view if there none and show an error view if the API call fails. Edit MainScreen:

...
import { connect } from 'react-redux';

import { 
  FETCH_EXPENSES,
  EXPENSES_LOADED,
  EXPENSES_LOAD_FAILED
} from "../../utils/Constants"
...

// execute fetch expense when component mounts
export default class MainScreen extends Component {
  ...
  componentDidMount() {
    this.props.fetchExpenses();
  }
  ...

  const mapStateToProps = state => ({
    isLoading: state.isLoading,
    expenses: state.expenses,
  })

  const mapDispatchToProps = dispatch => ({
    fetchExpenses: () => dispatch({type: FETCH_EXPENSES})
  })

  export default connect(mapStateToProps, mapDispatchToProps)(MainScreen)

  ...

Now you can use conditional rendering to display certain components when a condition is meet. For example we show an Loader when the app is fetching expenses from the API. (this.state.isLoading = true)

return (
        <View style={styles.container}>
          <View style={styles.headerStyle}>
            <Button
              buttonStyle={styles.buttonStyle}
              onPress={() => this.props.navigation.navigate('AddExpenseScreen')}
              title="Add new"
              titleStyle={styles.buttonTitleStyle}
            />
          </View>    
          {
            this.props.isLoading && 
            <ActivityIndicator
              animating={true}
              style={styles.indicator}
              size="large"
            />
          }
          {
            !this.props.isLoading && 
            <View>
              {
                this.props.expenses.count === 0 && 
                <Text>No Expenses</Text>
              }
              {
                this.props.expenses.count !== 0 && 
                <FlatList
                  keyExtractor={this.keyExtractor}
                  data={this.props.expenses.items}
                  renderItem={this.renderItem}
                />
              }
            </View>
          }
        </View>
    )
}

Part 4: Adding localization

I've tried multiple localization libraries for React Native but finally settled for i18next.

Add the dependency:

$ yarn add i18next --save

To keep things simple, all translations will be in the same folder. In a bigger app, it is recommended to keep component specific translations alongside the component, that way it is easier to clean up if you decide to delete a component, reuse it in another project, etc.

Create a file i18n/index.js:

import i18next from 'i18next'

i18next.init({
  lng: 'en',
  debug: false,
  resources: {
    en: { app: require('./en.json') },
    fr: { app: require('./fr.json') }
  }
})

Create a json file for each supported language. e.g.:

{
  "appName": "Gestionnaire de Dépenses",
  "cancel": "Annuler",
  "addExpense": "Ajouter une dépense",
  "expenseLabel": "Dépense",
  "amount": "Montant",
  "amountInEuros": "montant en euros: ",
  "category": "Catégorie",
  "categoryPlaceholder": "Choisir une catégorie..",
  "date": "Date",
  "comment": "Commentaire"
}

finally call the i18n initialization in your App.js:

...
import './i18n'

To translate a string in a component, import i18n then call i18next.t:

...
    headerTitle: i18next.t('app:appName'),
...

Get current system locale and wiring it

We will use RN NativeModules to get the current device locale, in i18n/index.js, add: source

...
import { Platform, NativeModules } from "react-native";

let langRegionLocale = "en_US";

// If we have an Android phone
if (Platform.OS === "android") {
  langRegionLocale = NativeModules.I18nManager.localeIdentifier || "";
} else if (Platform.OS === "ios") {
  langRegionLocale = NativeModules.SettingsManager.settings.AppleLocale || "";
}

// "en_US" -> "en", "es_CL" -> "es"
let languageLocale = langRegionLocale.substring(0, 2); // get first two characters

i18next.init({
  lng: languageLocale,  <-- set current locale
  fallbackLng: 'en',
  debug: false,
  resources: {
    en: { app: require('./en.json') },
    fr: { app: require('./fr.json') }
  }
})

Part 5: Make the app work offline

In order to make the app usable when no Internet connection is available we will use some neat libraries. One of these are redux-persist, which enables to save the state store of the app to local storage. Then we will need redux-offline-queue which makes it possible for the user to add new expenses when offline. When a connection is detected, all changes will be synced transparently.

Add the dependencies:

$ yarn add redux-offline-queue --save

Import Redux Offline Queue reducer, in reducers/index.js:

import { combineReducers } from "redux"
import DataReducer from "./Reducer"

const AppReducer = combineReducers({
  offlineQueue: require('redux-offline-queue').reducer,
  data: DataReducer
})

export default AppReducer

Important Notice above that we named our DataReducer as data, so in order to access it you will need to change calls to the state to this.props.data.expenses for example

Now, we need to integrate the ROQ middleware with our Redux Saga configuration in our App.js:

...
import {
  offlineMiddleware,
  suspendSaga,
  consumeActionMiddleware,
} from 'redux-offline-queue'

const sagaMiddleware = suspendSaga(createSagaMiddleware())

const store = createStore(
  AppReducer,
  applyMiddleware(
    offlineMiddleware(),
    sagaMiddleware,
    logger,
    consumeActionMiddleware()
  )
)
sagaMiddleware.run(dataSaga)
...

It's important to make sure the consumeActionMiddleware is the last to being called. Configure Network Connectivity Saga

We will need to separate the Data Saga from the connectivity specific saga. So rename sagas/index.js to sagas/DataSaga.js. Create a new sagas/index.js:

import { fork, all } from 'redux-saga/effects'

/* ------------- Sagas ------------- */

import AppStateRootSaga from './AppStateSaga'
import DataSaga from './DataSaga'

/* ------------- Connect Types To Sagas ------------- */

export default function* root() {
  yield all([
    fork(AppStateRootSaga),
    fork(DataSaga),
  ])
}

Create sagas/AppStateRootSaga.js

import { eventChannel } from 'redux-saga'
import { put, take, fork, all } from 'redux-saga/effects'
import { NetInfo } from 'react-native'

import { OFFLINE, ONLINE } from 'redux-offline-queue'

/**
 * Launches connectivity state watcher.
 *
 * This is inifinite loop that reacts to NetInfo.isConnected change.
 * The connectivity state is saved to redux store.
 */
export function* startWatchingNetworkConnectivity() {
  const channel = eventChannel((emitter) => {
    NetInfo.isConnected.addEventListener('connectionChange', emitter)
    return () => NetInfo.isConnected.removeEventListener('connectionChange', emitter)
  })
  try {
    for (; ;) {
      const isConnected = yield take(channel)

      if (isConnected) {
        yield put({ type: ONLINE })
      } else {
        yield put({ type: OFFLINE })
      }
    }
  } finally {
    channel.close()
  }
};

export default function* root() {
  yield all([
    fork(startWatchingNetworkConnectivity),
  ])
}

The saga above will listen for all connectivity changes and save the connectivity status in the store.

At this point our state store looks like this :

Example

Configure data persistence with redux-persist

Redux Persist enables to persist the state store to localStorage when the app is killed.

Add the dependency:

$ yarn add redux-persist --save

Edit App.js to add the persistence config:

...
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'


const persistConfig = {
  key: 'root',
  storage,
}

const persistedReducer = persistReducer(persistConfig, AppReducer)
...
const persistor = persistStore(store)
export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          <Navigator />
        </PersistGate>
      </Provider>
    )
  }
}

Part 6: Add connection status Banner

We are going to use the AppStateRootSaga we setup previously to show a banner to the user to indicate when he is online or not.

Use the following Banner Component credits:

import React from "react";
import { View, Text } from "react-native";

const NetworkStatusBanner = ({ isConnected, isVisible }) => {
  if (!isVisible) return null;

  const boxClass = isConnected ? "success" : "danger";
  const boxText = isConnected ? "online" : "offline";
  return (
    <View style={[styles.alertBox, styles[boxClass]]}>
      <Text style={styles.statusText}>you are {boxText}</Text>
    </View>
  )
}

const styles = {
  alertBox: {
    padding: 5
  },
  success: {
    backgroundColor: "#88c717"
  },
  danger: {
    backgroundColor: "#f96161"
  },
  statusText: {
    fontSize: 14,
    color: "#fff",
    alignSelf: "center"
  }
};

export default NetworkStatusBanner;

First map the offline.isConnected property in the MainScreen :

...
const mapStateToProps = (state) => {
  return {
    isLoading: state.data.isLoading,
    expenses: state.data.expenses,
    isOnline: state.offline.isConnected
  }
}
...

Now on each state update, check the connectivity value:

// MainScreen 
...

async componentDidUpdate (prevProps, prevState) {
    const { isOnline } = this.props

    if (isOnline && prevProps.isOnline != isOnline) {
      this.props.fetchExpenses()
    }
  }

...

Include the NetworkStatusBanner:

...
<View style={styles.container}>
          <View style={styles.headerStyle}>
            <Button
              buttonStyle={styles.buttonStyle}
              onPress={() => this.props.navigation.navigate('AddExpenseScreen')}
              title={i18next.t('app:addExpense')}
              titleStyle={styles.buttonTitleStyle}
            />
          </View>
          <NetworkStatusBanner
              isConnected={this.props.isOnline}
              isVisible={!this.props.isOnline}
            />
...

Part 7: Testing Redux with Jest

Install redux-mock-store, this library will help us mock redux's state in our tests.

yarn add redux-mock-store --dev

We are not going to use react-test-renderer, instead add Enzyme:

yarn add enzyme enzyme-adapter-react-16 enzyme-to-json --dev

We will use jest to implement tests for our redux state. In __tests__ folder, add spec files for each component we want to test. For example, test that the SplashScreen is rendered without crashing:

import React from 'react'
import { render } from 'react-native-testing-library'
import SplashScreen from '../../App/components/screens/SplashScreen'

describe('SplashScreen', () => {
  it('renders the screen without crashing', () => {
    const rendered = render(<SplashScreen />).toJSON()
    expect(rendered).toBeTruthy()
  })  
})