1. Developer Tutorials
  2. App Development Best Practices

App Development Best Practices

In this tutorial, you will learn about the best practices that you need to know to help you better organize your App code and allow you to have a more efficient development experience.

Have separate directories for your React Native Components

Components and Containers should be divided into two directories. These directories can have different names based on your own preference. Separating these directories will be helpful to write highly readable clean codes.

Containers directory should follow these rules:

  • Containers should not import anything from react-native (View, Text, etc.) that is used to build your JSX components.
  • Imports to your Higher-Order Components connecting to the Redux store and their respective connections must be here. This means that Redux hooks, Redux actions, Redux selectors must be put here.
  • React-navigation related integrations and a unique library you use for your project are other imports you can possibly place here.
// containers
import React from 'react'
import type { Element } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { NavigationScreenProp } from 'react-navigation'
import I18n from 'react-native-i18n'
import { Formik } from 'formik'
import { registerProcess } from 'actions'
import { authenticationSelector, ordersLoading } from 'selectors'

Components directory should follow these rules:

  • Components should receive the App State data as props that would be used to construct the UI. All the JSX code of your React components and their respective react-native imports should be put here.
  • The react hooks (useEffect, useRef, useState) should be utilized at the component level. 
  • Types, styles, components for code reuse, and any other imports that the container does not do must be here.
// components
import React, { useRef, useState } from 'react'
import {
  Animated,
  View,
  TouchableOpacity,
  Text,
} from 'react-native'
import { ArrowIcon } from 'components/icons'
import NutritionDetails from './NutritionDetails'
import type { NutritionalInfoType } from 'types/product'
import { nutritionalInfoStyles as styles } from './styles'

Create Aliases

Use babel-plugin-module-resolver in creating aliases to avoid nested imports such as import Product from '../../../Components/Product'. The aliases created should look something like this:

alias: {
          actions: './app/actions',
          api: './app/api',
          assets: './app/assets',
          components: './app/components',
          containers: './app/containers',
          constants: './app/constants',
          sagas: './app/sagas',
          selectors: './app/selectors',
          types: './app/types',
          utils: './app/utils',
        }

After setting this up, imports such as import Product from 'components/Product' can be used.

Sort your imports logically

As much as possible, divide and sort your imports logically. There is no specific rule to follow on the way you should be sorting them, but at least categorizing them would be helpful.

import React, { useState } from 'react'
import type { Element } from 'react'
import { View, ScrollView } from 'react-native'
import { NavigationScreenProp } from 'react-navigation'
import { useSelector } from 'react-redux'
import type { OrderType, LineItemWithProdType } from 'types'
import { ordersByIdSelector, productsByIdSelector } from 'selectors'
import { OrderTicket, OrderDetails } from 'components/Order'
import { DefaultStatusBar, RedStatusBar } from 'components/StatusBar'
import { orderStyles as styles } from './styles'

Declare the types

It is necessary to declare the types of every object in the code, to define whether it is TypeScript or Flow. This includes return type and argument type.

const payload: LoginUserType = {
  email: '[email protected]',
  password: 'password',
}
const roundDistance = (distance: number): string => (distance / 1000).
toFixed(1)

For flow-based projects, add // @flow at the start of every new file you will create. 

It is important to note that the types created must be reused across the code following the DRY principles. This will avoid trouble like when you decide to declare types in each component and it gets left unorganized into separate types directories.

To create exact object types for even stronger type safety, use the | symbol. This will ensure that no new keys will be added to an object.

type LoginUserType = {|
  email: string,
  password: string,
|}

Separate your Styles

Separate your styles away from your React components. This will make your code cleaner and easier to review. 

You may refer to this article from Thoughtbot for the styling requirements of a project: React Native style guide

import { productAmountstyles as styles } from './styles'
...
<View style={styles.container}>
  <Text style={styles.amountText}>{I18n.t('itemScreen.amount')}</Text>
  <View style={styles.quantityContainer}>
    <CircularButton disabled={quantity === 1} onPress={onReduce} />
    <View style={styles.quantityTextContainer}>
      <Text style={styles.quantityText}>{quantity}</Text>
    </View>
    <CircularButton disabled={false} add onPress={onAdd} />
  </View>
</View>

Your components must hook

Why use hooks? Aside from Hooks being declarative and easy to read and understand, they also reduce a lot of  this.x, this.y and this.setState() in your code. There will be no need to use a class-based component that a functional component using hooks cannot solve. 

const [showModal, setShowModal] = useState(true)
...
useEffect(() => {
  dispatch(doFetchOrders())
  dispatch(doFetchProducts())
}, [dispatch])
...
const authenticationData: AuthenticationStateType = useSelector
(authenticationSelector)
...
const mapViewRef: { current: MapView } = useRef(null)

Let Redux manage

Redux has a predictable and useful way to manage App state in projects unless you are using GraphQL or its variants which will not need state management for the front-end. This will work even better with Immer to gain mutating capabilities. Also, setting up React Native debugger is suggested.

Write sagas for asynchrony

It is important to consider the advantages of using redux-saga. This helps you handle the App side effects of asynchronous logic such as API calls, navigation to another screen, etc. And will make it more manageable. 

Also, we can use axios to handle API calls. Using it over a library like fetch will have its advantages due to its granular error handling.

Aggregate the selectors

Putting the logic of extracting useful data from the App state in one place under a  selectors directory is highly suggested to allow individual functions to be reused across components. In addition, using the reselect library that comes with caching and memoization benefits resulting in efficient computation of derived data is also recommended.

export const productsSelector = state => state.menuItems.products.map(({ 
  product }) => product)

export const productsByIdSelector = createSelector(
  productsSelector,
  (products) => products.reduce((prodObj, product) => (
    prodObj[product.id] = product), {}),
)
...
const productsById = useSelector(productsByIdSelector)

Testing Code

To have fewer bugs, Test-driven development and writing clear tests are essential.

There are a number of ways to test different parts of React Native Apps. It is recommended to at least implement the Snapshot tests and Redux tests (actions, reducers, sagas, and selectors). 

What is the importance of Snapshot tests? 

Snapshot tests ensure that the components do not break and gives an overview of the UI changes that were introduced by the code.

it('renders ArrowIcon', () => {
  const component = renderer.create(<ArrowIcon />)
  const tree = component.toJSON()
  expect(tree).toMatchSnapshot()
})

What is the importance of Redux tests?

Redux tests ensure the state of the App changes in a predictable manner with the current code. Its architecture and tests being specific are the main advantages of using Redux. Full code coverage should be targeted here.

// actions
it('gets all products', () => {
  const action = doFetchProducts()
  const expectedAction = { type: 'PRODUCTS_FETCH' }
  expect(action).toEqual(expectedAction)
})

// reducers
it('should handle FORGOT_PASSWORD_DONE', () => {
  const forgotPasswordDone = {
    type: FORGOT_PASSWORD_DONE,
    payload: {
      email: '[email protected]',
    },
  }
  Object.freeze(beforeForgotPasswordDone)
  const newStateAfterForgotPassword = authReducer(beforeForgotPasswordDone, 
  forgotPasswordDone)
  expect(newStateAfterForgotPassword).toEqual(afterForgotPasswordDone)
})

// selectors
it('productsByIdSelector should return all the products by Id', () => {
  expect(productsByIdSelector(mockedState)).toEqual(productsById)
})

Questions?

We're always happy to help with questions you might have! Search our documentation, contact support, or connect with our sales team.