/crna-recipe

Step-by-step guide to bootstrap a React Native app from scratch

React Native App Creation Recipe

This is a step-by-step guide to create React Native app for Atolye15 projects. You can review React Native App Starter project to see how your application looks like when all steps followed.

You will get an application which has;

  • TypeScript
  • Linting
  • Formatting
  • Testing
  • CI/CD
  • Storybook

Table of Contents

Step 1: Installing the React Native CLI

First of all, we need to install the React Native command line interface.

yarn global add react-native-cli

Step 2: Creating a new app

Use the React Native command line interface to generate a new React Native project called "AwesomeProject":

react-native init AwesomeProject --template typescript

NOTE: Project name should be alphanumeric!

Step 3: Make TypeScript more strict

We want to keep type safety as strict as possibble. In order to do that, we update tsconfig.json with the settings below. Also we prefer to disable isolatedModules and activate skipLibCheck.

"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"isolatedModules": false,

Step 4: Installing Prettier

We want to format our code automatically. So, we need to install Prettier.

yarn add prettier --dev
// .prettierrc

{
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "all"
}

Also, we want to enable format on save on VSCode.

React Native CLI adds .vscode to .gitignore, but we prefer not to ignore. So remove it from .gitignore.

// .vscode/settings.json

{
  "editor.formatOnSave": true
}

Finally, we update package.json with related format scripts.

"format": "prettier --write 'src/**/*.{ts,tsx}'",
"format:check": "prettier -c 'src/**/*.{ts,tsx}'"

Step 5: Installing ESLint

We want to have consistency in our codebase and also want to catch mistakes. So, we need to install ESLint.

yarn add eslint eslint-config-airbnb eslint-config-prettier eslint-plugin-eslint-comments eslint-plugin-import eslint-plugin-jest eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-native @typescript-eslint/eslint-plugin @typescript-eslint/parser --dev
// .eslintrc

{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "airbnb",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "plugin:eslint-comments/recommended",
    "plugin:import/errors",
    "plugin:import/warnings",
    "plugin:import/typescript",
    "plugin:jest/recommended"
  ],
  "env": {
    "browser": true,
    "jest": true,
    "react-native/react-native": true
  },
  "plugins": [
    "react",
    "react-native",
    "@typescript-eslint",
    "jsx-a11y",
    "import",
    "prettier",
    "jest",
    "eslint-comments"
  ],
  "rules": {
    "@typescript-eslint/indent": "off",
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/no-use-before-define": "off",
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }],
    "react/prop-types": "off",
    "react/button-has-type": "off",
    "no-use-before-define": "off",
    "import/no-extraneous-dependencies": [
      "error",
      {
        "devDependencies": [
          "storybook/**/*.{ts,tsx,js}",
          "config-overrides.js",
          "src/setupTests.ts",
          "src/components/**/*.stories.tsx",
          "src/styles/**/*.stories.tsx",
          "src/**/*.test.{ts,tsx}"
        ]
      }
    ],
    "react-native/no-unused-styles": "error",
    "react-native/no-inline-styles": "error",
    "react-native/no-color-literals": "error",
    "react/jsx-one-expression-per-line": "off",
    "@typescript-eslint/explicit-member-accessibility": "off",
    "prettier/prettier": ["error"]
  },
  "overrides": [
    {
      "files": ["*.style.ts"],
      "rules": {
        "@typescript-eslint/camelcase": "off"
      }
    },
    {
      "files": ["*.stories.tsx", "*.test.tsx"],
      "rules": {
        "@typescript-eslint/no-explicit-any": "off",
        "react-native/no-color-literals": "off",
        "react-native/no-inline-styles": "off"
      }
    }
  ]
}

also ignore some files/folders;

# .eslintignore

ios
android
build
coverage

# Storybook
storybook/storyLoader.js

We need to update package.json for ESLint scripts.

"lint:eslint": "eslint 'src/**/*.{ts,tsx}'",
"lint:ts": "tsc && yarn lint:eslint",
"lint": "yarn lint:ts",
"format": "prettier --write 'src/**/*.{ts,tsx}' && yarn lint:eslint --fix",

Finally, we need to enable prettier ESLint integration on VSCode.

// .vscode/settings.json

{
  // ... ,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    { "language": "typescript", "autoFix": true },
    { "language": "typescriptreact", "autoFix": true }
  ]
}

Step 6: Setting up our test environment

We'll use jest with react-native-testing-library.

yarn add react-native-testing-library --dev

Add the following script into package.json

"test": "jest",
"test:watch": "yarn test --watch",
"coverage": "yarn run test --coverage"

and then update jest.config.js as follows to complete jest configuration.

{
  // ... ,
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/index.tsx',
    '!src/setupTests.ts',
    '!src/components/**/index.{ts,tsx}',
    '!src/**/*.stories.{ts,tsx}',
    '!src/**/*.style.ts',
    '!src/styles/**/*',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
}

Let's add a simple test to verify our setup.

// src/App.test.tsx

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

import App from './App';

it('renders correctly', () => {
  const comp = shallow(<App />);

  expect(comp.output).toMatchSnapshot();
});

Also, verify coverage report with yarn coverage.

When you run yarn coverage, a folder named coverage will be created in the root directory. This folder is auto-generated file. We should add it to .gitignore

# .gitignore

...
# Test Coverage
coverage

Step 7: Setting up config variables

We use the react-native-config package to expose config variables to our javascript code in React Native.

Follow these steps to install.

Step 8: Organizing Folder Structure

Our folder structure should look like this;

src/
├── App.test.tsx
├── App.tsx
├── __snapshots__
│   └── App.test.tsx.snap
├── components
│   └── Button
│       ├── Button.style.ts
│       ├── Button.stories.tsx
│       ├── Button.test.tsx
│       ├── Button.tsx
│       ├── __snapshots__
│       │   └── Button.test.tsx.snap
│       └── index.ts
├── containers
│   └── Like
│       ├── Like.tsx
│       └── index.ts
├── index.tsx
├── screens
│   ├── Feed
│   │   ├── Feed.style.ts
│   │   ├── Feed.test.tsx
│   │   ├── Feed.tsx
│   │   ├── index.ts
│   │   └── tabs
│   │       ├── Discover
│   │       │   ├── Discover.style.ts
│   │       │   ├── Discover.test.tsx
│   │       │   ├── Discover.tsx
│   │       │   └── index.ts
│   │       └── MostLiked
│   │           ├── MostLiked.test.tsx
│   │           ├── MostLiked.tsx
│   │           └── index.ts
│   ├── Home
│   │   ├── Home.style.ts
│   │   ├── Home.test.tsx
│   │   ├── Home.tsx
│   │   └── index.ts
│   └── index.ts
├── styles
│   ├── Colors.ts
│   ├── Spacing.ts
│   ├── Typography.ts
│   └── index.ts
└── utils
    ├── location.test.ts
    └── location.ts

Step 9: Adding Storybook

We need to initialize the Storybook on our project. We'll use automatic setup with a few edits:

npx -p @storybook/cli sb init --type react_native

Warning: Probably after you have run the command above, you'll be asked to select a version. Cancel it.

Storybook CLI automatically installs v5.0.x, however v5.0.x is an unpublished version for react-native, therefore problems arise during installation. In order to avoid this problem we're going to fix our storybook packages in our package.json file to latest stable version 4.1.x. (Check this issue for more information.)

"@storybook/addon-actions": "^4.1.16",
"@storybook/addon-links": "^4.1.16",
"@storybook/addons": "^4.1.16",
"@storybook/react-native": "^4.1.16",

thereafter in order to activate the changes and update yarn.lock file we'll run code below;

yarn

After completing steps above you'll notice that storybook CLI have created storybook folder on your project's root folder. We'll customize this folder structure according to our use case.

Firstly change the name of index.js file in storybook folder to storybook.ts. Also change file extensions of other files from js to ts, except the addons.js file (storybookjs/storybook#3970).

After that, we create a new file named index.ts to expose StorybookUI in your app.

// storybook/index.ts

import StorybookUI from './storybook';

export default StorybookUI;

We finished the storybook installation but we are not done yet;

The stories for our app will be inside the src/components directory with the .stories.tsx extension.The React Native packager resolves all the imports at build-time, so it's not possible to load modules dynamically. we need to use a third party loader react-native-storybook-loader to automatically generate the import statements for all stories.

yarn add react-native-storybook-loader --dev

You need to update storybook.ts as follows:

Note: Do not forget to replace %APP_NAME% with your app name

// storybook/storybook.ts

import { AppRegistry } from 'react-native';
import { getStorybookUI, configure } from '@storybook/react-native';
import { loadStories } from './storyLoader';

import './rn-addons';

// import stories
configure(() => {
  loadStories();
}, module);

// Refer to https://github.com/storybooks/storybook/tree/master/app/react-native#start-command-parameters
// To find allowed options for getStorybookUI
const StorybookUIRoot = getStorybookUI({});

// If you are using React Native vanilla write your app name here.
// If you use Expo you can safely remove this line.
AppRegistry.registerComponent('%APP_NAME%', () => StorybookUIRoot);

export default StorybookUIRoot;

The file storyLoader.js that we imported above is an auto-generated file. We should add it to .gitignore.

# .gitignore

...
# Storybook
storybook/storyLoader.js

After you install storybook loader, you should run the following command once to avoid typescript errors.

yarn rnstl

Update the storybook script into package.json as follows:

"storybook": "watch rnstl ./src --wait=100 | storybook start | yarn start --projectRoot storybook --watchFolders $PWD"

Add the following config into package.json:

// package.json
{
  "config": {
    "react-native-storybook-loader": {
      "searchDir": ["./src"],
      "pattern": "**/*.stories.tsx",
      "outputFile": "./storybook/storyLoader.js"
    }
  }
}

Warning: If you get typescript errors related with the storybook, you should disable isolatedModules in tsconfig.json

Lastly, because we use typescript in the project, we need to install the type definition for storybook.

yarn add @types/storybook__react-native --dev

Let's create an example story for our Button component.

// src/components/Button/Button.stories.tsx

import React from 'react';
import { storiesOf } from '@storybook/react-native';

import Button from './Button';

storiesOf('Button', module)
  .add('Primary', () => <Button theme="primary">Primary Button</Button>)
  .add('Secondary', () => <Button theme="secondary">Secondary Button</Button>);

Step 10: Adding CircleCI config

We can create a CircleCI pipeline in order to CI / CD.

# .circleci/config.yml

version: 2
jobs:
  build_dependencies:
    docker:
      - image: circleci/node:10
    working_directory: ~/repo
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - restore_cache:
          keys:
            - dependencies-{{ checksum "package.json" }}
            - dependencies-
      - run:
          name: Install
          command: yarn install
      - save_cache:
          paths:
            - ~/repo/node_modules
          key: dependencies-{{ checksum "package.json" }}
      - persist_to_workspace:
          root: .
          paths: node_modules

  test_app:
    docker:
      - image: circleci/node:10
    working_directory: ~/repo
    steps:
      - checkout
      - attach_workspace:
          at: ~/repo
      - run:
          name: Generate Storyloader
          command: yarn rnstl
      - run:
          name: Lint
          command: yarn lint
      - run:
          name: Format
          command: yarn format:check
      - run:
          name: Coverage
          command: yarn coverage

workflows:
  version: 2
  build_app:
    jobs:
      - build_dependencies
      - test_app:
          requires:
            - build_dependencies

After that we need to enable CircleCI for our repository.

Step 11: Github Settings

We want to protect our develop and master branches. Also, we want to make sure our test passes and at lest one person reviewed the PR. In order to do that, we need to update branch protection rules like this in GitHub;

github-branch-settings

Step 12 Final Touches

We are ready to develop our application. Just a final step, we need to update our README.md to explain what we add a script so far.

EXAMPLE_README

Step 13: Starting to Development 🎉

Everything is done! You can start to develop your next awesome React Native application now on 🚀

Bonus: Npm Script Aliases

React-Native Alias

yarn rn

rn alias for react-native allows to run react-native CLI command via locally installed react-native.

// package.json

"rn": "react-native",

NOTE: Only works with yarn.

Run On Aliases

yarn ios
yarn run ios

yarn android
yarn run android

ios and android aliases are helpful when we need to pass different parameter for our project and provides single point entry.

// package.json

"ios": "yarn rn run-ios",
"android": "yarn rn run-android",

Example

If we want to run our app on iPhone X as default and with scheme just specify that in the alias.

// package.json

"ios": "yarn rn run-ios --simulator 'iPhone X' --scheme 'Production'",

Clear React Native Cache Alias

yarn clear-rn-cache
// package.json

"clear-rn-cache": "watchman watch-del-all && rm -rf $TMPDIR/react-* && rm -rf $TMPDIR/metro* && rm -rf $TMPDIR/haste-*"

Related