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) })