An efficient way to structure React Native projects

react native project structure

Let’s discuss a structure to start new projects or when you need to scale large projects.

We will use React Native project structure as a basis for this architecture, but the concepts can be leveraged in projects using other frameworks.

For the purpose of this post, I will use the following patterns and packages:

Follow the required installation steps on Getting Started · React Native.

After configuring the React Native CLI on your machine, verify if  `react-native -v` is available on your terminal.

You should get a return similar to this:


$ react-native-cli: 2.0.1
$ react-native: n/a - not inside a React Native project directory
 

So we can proceed to create our project.

Cheesecake Labs is the top #5 React Native Development Company and has delivered quality cross-platform applications to a number of clients.

Creating a project from Scratch

A page with 'Give it a try' written on the top. It has two steps: Run this and Read these.

Choose a directory of your choice to create our base project using the following command:


$ react-native init ReactNativeCklExample

After the execution, you can access the directory using the cd ReactNativeCklExample. You will get a structure similar to this:


.
├── __tests__
│   ├── App-test.js
├── android
├── ios
├── node_modules
├── .buckconfig
├── .eslintrc.js
├── .flowconfig
├── .gitattributes
├── .gitignore
├── .prettierrc.js
├── .watchmanconfig
├── App.js
├── app.json
├── babel.config.js
├── index.js
├── metro.config.js
├── package.json
└── yarn.lock
 

Some of the most popular mobile apps were built with the React Native framework. You probably have a few installed on your own smartphone. Here’s how the world’s most innovative brands use React Native.

Structuring the pillars

A wall of colorful bricks like Lego.
Photo by Omar Flores

We will define a structure for our project to grow efficiently and make its maintenance easier.

Our first step will be to define the directory structure within `src` (Source). This directory will contain all major project files.

Let’s create the following initial structure for our project:


.
├── src
│   ├── assets
│   │  ├── fonts
│   │  ├── images
│   ├── components
│   │  ├── atoms
│   │  ├── molecules
│   │  ├── organisms
│   ├── navigations
│   ├── scenes
│   ├── styles
│   ├── utils
│   ├── index.js
 

Right now you might be wondering why there are so many folders and files, but don’t worry, further on the post we’ll go over their purpose and how important each one of them is.

Enabling the use of the alias

To simplify the require/import of paths in our project, we must configure directory aliases. So let’s install the following packages:


$ yarn add -D eslint-import-resolver-babel-module@^5.1.0 
eslint-plugin-import@^2.18.2 babel-plugin-module-resolver@^3.2.0
 

After installing the dependencies, let’s configure the .babelrc:

.babelrc


{
  "plugins": [
    [
      "module-resolver",
      {
        "cwd": "babelrc",
        "root": ["./src"],
        "extensions": [".js", ".ios.js", ".android.js"],
        "alias": {
          "_assets": "./src/assets",
          "_components": "./src/components",
          "_atoms": "./src/components/atoms",
          "_molecules": "./src/components/molecules",
          "_organisms": "./src/components/organisms",
          "_navigations": "./src/navigations",
          "_scenes": "./src/scenes",
          "_services": "./src/services",
          "_styles": "./src/styles",
          "_utils": "./src/utils"
        }
      }
    ]
  ]
}

Edit the .eslintrc.js file to avoid lint errors when using the new alias:


module.exports = {
  root: true,
  extends: '@react-native-community',
  plugins: ['import'],
  settings: {
    'import/resolver': {
      node: {
        paths: ['src'],
        alias: {
          _assets: './src/assets',
          _components: './src/components',
          _atoms: './src/components/atoms',
          _molecules: './src/components/molecules',
          _organisms: './src/components/organisms',
          _navigations: './src/navigations',
          _scenes: './src/scenes',
          _services: './src/services',
          _styles: './src/styles',
          _utils: './src/utils',
        },
      },
    },
  },
};

Read more about the alias setup at Babel Plugin Module Resolver.

Enable editors to alias autocompletion

Create the jsconfig.json file and use the same alias that was defined in .babelrc. Check it out below:


{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "_assets": ["src/assets/*"],
      "_components": ["src/components/*"],
      "_atoms": ["src/components/atoms/*"],
      "_molecules": ["src/components/molecules/*"],
      "_organisms": ["src/components/organisms/*"],
      "_navigations": ["src/navigations/*"],
      "_scenes": ["src/scenes/*"],
      "_services": ["src/services/*"],
      "_styles": ["src/styles/*"],
      "_utils": ["src/utils/*"]
    }
  }
}

Once you have edited it, it’s time to test the alias. Let’s edit our `src/index.js` file by adding a test component as follows:


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

const App = () => (
  
    Hello World
  
);

export default App;

Now in our index.js in the project root we will import the App component as follows:


import App from './src';

This way you will have your alias set up working on your project.

Atomic Components

5 icons representing atoms, molecules, organisms, templates and pages.
Illustration by Brad Frost

For a better understanding of atomic systems/components, I indicate reading the following article: Atomic Design with React.

We will not dive into the concepts of atomic, only the organization we have chosen to use in this project:

  • Atoms: the smallest possible components, such as buttons, titles, inputs or event color pallets, animations, and fonts.
  • Molecules: are the composition of one or more components of atoms.
  • Organisms: the combination of molecules that work together or even with atoms that compose more elaborate interfaces.

Remembering the directories we use to organize your components:


.
├── src
│   ├── components
│   │  ├── atoms
│   │  ├── molecules
│   │  ├── organisms

Each component directory has an index.js file that exports the specified category.

Let’s create a component called HelloWorld in src/atoms to understand the idea:

src/atoms/hello-world.js


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

const HelloWorld = ({name}) => Hello World {name}!;

export default HelloWorld;

We export as follows: 

src/atoms/index.js

export {default as HelloWorld} from ‘./hello-world’;

Now we can use this component in src/index.js:


import React from 'react';
import {HelloWorld} from '_atoms'; 

const App = () => <

export default App;

Note: The App.js in the project root can be removed, it will no longer be used.

Our Scenes

I have a habit of dividing every application screen as a scene and so each one has its directory.

We will define some scenes that will not be used this time, and then we will configure these navigations using the created screens.


.
├── src
│   ├── scenes
│   │  ├── login
│   │  │	 ├── index.js // LoginScreen
│   │  ├── home
│   │  │	 ├── index.js // HomeScreen
│   │  ├── about
│   │  │	 ├── index.js // AboutScreen

At the Login screen, we will add a navigation button to go to the Home screen. See below:


import React from 'react';
import {SafeAreaView, Text, TouchableHighlight} from 'react-native';

const LoginScreen = ({navigation}) => (
  
    Screen: Login

     navigation.navigate('Home')}>
      Go to home
    
  
);

export default LoginScreen;

Note: The navigation object will be available on all screens that are surrounded by the navigator object.

Ways of Navigation

Picture of the point of view of the bow region of a boat.
Photo by Joseph Barrientos

In this step, we will need to add some new dependencies to the project. See below:


$ yarn add react-navigation@^4.0.0 react-navigation-stack@^1.5.3 
react-navigation-tabs@^2.4.0 react-native-gesture-handler@^1.4.1 
react-native-reanimated@^1.2.0

You can read more about it here.

In our application, we will have two types of navigation.

In the Login screen, we will have Stack navigation type and in the rest of the app we will have Tab navigation type.

Note: This is just an example of navigation, not a pattern. If you need to use other types of navigation, you can create them at the src/navigations.

In the src/navigations directory we will define the following structure:


.
├── src
│   ├── navigations
│   │  ├── index.js            // RootNavigator
│   │  ├── auth-navigator.js   // AuthNavigator
│   │  ├── app-navigator.js    // AppNavigator
  1. In the auth-navigator.js we will define the navigation type for the login screen:

import {createStackNavigator} from 'react-navigation-stack';

import LoginScreen from '_scenes/login';

const AuthNavigatorConfig = {
  initialRouteName: 'Login',
  header: null,
  headerMode: 'none',
};

const RouteConfigs = {
  Login:LoginScreen,
};

const AuthNavigator = createStackNavigator(RouteConfigs, AuthNavigatorConfig);

export default AuthNavigator;
  1. In the app-navigator.js we will define the type of navigation to internal screens app:

import {createBottomTabNavigator} from 'react-navigation-tabs';

import HomeScreen from '_scenes/home';
import AboutScreen from '_scenes/about';

const TabNavigatorConfig = {
  initialRouteName: 'Home',
  header: null,
  headerMode: 'none',
};

const RouteConfigs = {
  Home:{
    screen:HomeScreen,
  },
  About:{
    screen:AboutScreen,
  },
};

const AppNavigator = createBottomTabNavigator(RouteConfigs, TabNavigatorConfig);

export default AppNavigator;
  1. In the index.js we will define our RootNavigator merging the auth and app navigators:

import {createAppContainer, createSwitchNavigator} from 'react-navigation';

import AuthNavigator from './auth-navigator';
import AppNavigator from './app-navigator';

const RootNavigator = createSwitchNavigator(
  {
    Auth: AuthNavigator,
    App: AppNavigator,
  },
  {
    initialRouteName: 'Auth',
  },
);

export default createAppContainer(RootNavigator);

Now you can import the Navigator object into your src/index.js as follows:


import React from 'react';

import Navigator from '_navigations';

const App = () => ;

export default App;

This way you will have simple and functional navigation.

Reusable Services

Not everything can be considered a component in React Native, a well-known approach used to create separate modules and in some cases containing business rules are the use of services.

Directory for creating services:

src/services

They can be shared with multiple screens and components in your project.

Commonly used to create services that make contact with external APIs and use the axios library that we mentioned at the beginning of the post.

Shared Styles

A yellow ball bouncing from between brackets to between parenthesis.
Illustration by Dominic Magnifico

Based in CSS preprocessor we use some default files in our style structure:


.
├── src
│   ├── styles
│   │  ├── index.js        // Export all          
│   │  ├── colors.js       // Colors pallet
│   │  ├── mixins.js       // Mixins to use CSSinJS
│   │  ├── spacing.js      // Paddings, margins and scale
│   │  ├── typography.js   // Fonts types and sizes

index.js


import * as Colors from './colors';
import * as Spacing from './spacing';
import * as Typography from './typography';
import * as Mixins from './mixins';

export { Typography, Spacing, Colors, Mixins };

colors.js


export const PRIMARY = '#1779ba';
export const SECONDARY = '#767676';
export const WHITE = '#FFFFFF';
export const BLACK = '#000000';

// ACTIONS
export const SUCCESS = '#3adb76';
export const WARNING = '#ffae00';
export const ALERT = '#cc4b37';

// GRAYSCALE
export const GRAY_LIGHT = '#e6e6e6';
export const GRAY_MEDIUM = '#cacaca';
export const GRAY_DARK = '#8a8a8a';

mixins.js


import {Dimensions,PixelRatio} from 'react-native';

const WINDOW_WIDTH = Dimensions.get('window').width;
const guidelineBaseWidth = 375;

export const scaleSize = size => (WINDOW_WIDTH/guidelineBaseWidth) * size;

export const scaleFont = size => size * PixelRatio.getFontScale();

function dimensions(top, right = top, bottom = top, left = right, property){
  let styles = {};

  styles[`${property}Top`] = top;
  styles[`${property}Right`] = right;
  styles[`${property}Bottom`] = bottom;
  styles[`${property}Left`] = left;

  return styles;
}

export function margin(top, right, bottom, left){
  return dimensions(top, right, bottom, left, 'margin');
}

export function padding(top, right, bottom, left){
  return dimensions(top, right, bottom, left, 'padding');
}

export function boxShadow(color, offset = {height:2,width:2},
                           radius = 8, opacity = 0.2){
  return {
    shadowColor: color,
    shadowOffset: offset,
    shadowOpacity: opacity,
    shadowRadius: radius,
    elevation: radius,
  };
}

spacing.js


import {scaleSize} from './mixins';

export const SCALE_18 = scaleSize(18);
export const SCALE_16 = scaleSize(16);
export const SCALE_12 = scaleSize(12);
export const SCALE_8 = scaleSize(8);

typography.js


import { scaleFont } from './mixins';

// FONT FAMILY
export const FONT_FAMILY_REGULAR = 'OpenSans-Regular';
export const FONT_FAMILY_BOLD = 'OpenSans-Bold';

// FONT WEIGHT
export const FONT_WEIGHT_REGULAR = '400';
export const FONT_WEIGHT_BOLD = '700';

// FONT SIZE
export const FONT_SIZE_16 = scaleFont(16);
export const FONT_SIZE_14 = scaleFont(14);
export const FONT_SIZE_12 = scaleFont(12);

// LINE HEIGHT
export const LINE_HEIGHT_24 = scaleFont(24);
export const LINE_HEIGHT_20 = scaleFont(20);
export const LINE_HEIGHT_16 = scaleFont(16);

// FONT STYLE
export const FONT_REGULAR = {
  fontFamily: FONT_FAMILY_REGULAR,
  fontWeight: FONT_WEIGHT_REGULAR,
};

export const FONT_BOLD = {
  fontFamily: FONT_FAMILY_BOLD,
  fontWeight: FONT_WEIGHT_BOLD,
};

This is our basic style settings to structure our application.

This way you can import into any of your components the following Typography, Spacing, Colors, Mixins objects, which will have access to the functionality of each style object.

Extra: Custom Font

To enable custom fonts you need to create the react-native.config.js in the project root and set the directory where your .ttf files are as follows:


module.exports = {
  assets:['./src/assets/fonts/'],
};

After that, you should run the `react-native link` to link your fonts to the iOS / Android native code.

Defining Utils

We have the src/utils directory where we add all our utility/helper methods that can be shared across our entire project.

Whenever you come across situations where you get caught repeating code is a good situation to create a util/helper.

Wrapping up

I have been working with this format on the last React Native projects I worked on and I can say that it helped me a lot regarding the organization and development of the project.

This is just one way we found to be productive and better organized among our team, I hope it helps you too.

Feel free to comment below if you have any questions, I’ll be happy to help you.

Enjoy programming!

The repository of this example is available at ReactNativeCklExample.

References

About the author.

Helder Burato Berto
Helder Burato Berto

I keep moving constantly in search of knowledge, improve the quality of work and also as a person.