██╗   ██╗██╗      █████╗ ██████╗ ██╗███╗   ███╗██╗██████╗     ██╗   ██╗ ██████╗ ██╗   ██╗██╗  ██╗
  ██║   ██║██║     ██╔══██╗██╔══██╗██║████╗ ████║██║██╔══██╗    ██║   ██║██╔═══██╗██║   ██║██║ ██╔╝
  ██║   ██║██║     ███████║██║  ██║██║██╔████╔██║██║██████╔╝    ██║   ██║██║   ██║██║   ██║█████╔╝
  ╚██╗ ██╔╝██║     ██╔══██║██║  ██║██║██║╚██╔╝██║██║██╔══██╗    ╚██╗ ██╔╝██║   ██║╚██╗ ██╔╝██╔═██╗
██╗╚████╔╝ ███████╗██║  ██║██████╔╝██║██║ ╚═╝ ██║██║██║  ██║     ╚████╔╝ ╚██████╔╝ ╚████╔╝ ██║  ██╗
╚═╝ ╚═══╝  ╚══════╝╚═╝  ╚═╝╚═════╝ ╚═╝╚═╝     ╚═╝╚═╝╚═╝  ╚═╝      ╚═══╝   ╚═════╝   ╚═══╝  ╚═╝  ╚═╝

Rocket takeoff

Starting React Native Project in 2026

Updated to Expo 54.

We will use Expo's create-expo-app command recommended by the docs.

Also, we will add ESLint, Prettier, and some custom configurations that will make our development process better.

TLDR You can use one command npx create-expo-app -t expo-ts to create a new React Native project with all tools already set up (see README for details) or follow instructions below. 🤓

Please refer to the official React Native and Expo documentation for more details. 🤩

General setup

We will need the Node.js and Git installed before we start.

Please check the docs to ensure everything is installed on your machine.

Now let's create a new app.

  1. Run npx create-expo-app@latest --template blank-typescript command.
  2. Type your project name.
  3. Change the directory to your project with cd <your-project-name> command.
  4. Run npm run start to start Metro Bundler.
  5. Press i to start the iOS simulator or a to run the Android emulator.📱

Absolute path imports

To use absolute path imports, e.g. import { ComponentA } from 'src/components/A' (notice path starts with src), we need to add baseUrl parameter to the compilerOptions of tsconfig.json.

{
"compilerOptions": {
"baseUrl": "./"
},
...
}
{
"compilerOptions": {
"baseUrl": "./"
},
...
}

Move App.tsx to src folder

It's good to have all source files in one place. So let's move App.tsx to src with mv App.tsx src command.

Next, we need to change the index.ts file inside our project's root to load the App component from the src folder:

import { registerRootComponent } from 'expo'

import App from 'src/App'

registerRootComponent(App)
import { registerRootComponent } from 'expo'

import App from 'src/App'

registerRootComponent(App)

Check code for errors

We can use TypeScript compiler and ESLint for this.

TypeScript Compiler

Let's add a new check-typescript script to our package.json.

...
"scripts": {
...
"check-typescript": "tsc --noEmit"
},
...
...
"scripts": {
...
"check-typescript": "tsc --noEmit"
},
...

Now we can run the npm run check-typescript command to check our code for errors with the TypeScript compiler.

ESLint

ESLint is a JavaScript linter that helps you find and fix errors in your code. It's a great tool to help you write better code and catch mistakes before they make it to production. In conjunction, you can use Prettier, a code formatter that ensures all the code files follow a consistent styling.

npx expo lint
npx expo lint

If you're using VS Code, install the ESLint extension to lint your code as you type.

Add a new check-eslint script to our package.json.

...
"scripts": {
...
"check-eslint": "eslint './src/**/*{js,ts,jsx,tsx}'"
},
...
...
"scripts": {
...
"check-eslint": "eslint './src/**/*{js,ts,jsx,tsx}'"
},
...

Now we can run the npm run check-eslint command to check our code for errors with ESLint. And npm run check-eslint --fix to fix errors automatically.

Lint script

Let's combine TypeScript and ESLint checks so we can run both at once.

Add a new lint script to our package.json.

...
"scripts": {
...
"lint": "npm run check-typescript && npm run check-eslint"
},
...
...
"scripts": {
...
"lint": "npm run check-typescript && npm run check-eslint"
},
...

Prettier

Prettier is an opinionated code formatter. Let's install it.

npx expo install -- --save-dev prettier
npx expo install -- --save-dev prettier

We will also need .prettierrc.js config file in the project root.

module.exports = {
semi: false,
trailingComma: 'none',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
useTabs: false,
}
module.exports = {
semi: false,
trailingComma: 'none',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
useTabs: false,
}

Sort imports

Unsorted imports look ugly. Also, it could be hard to read and add new imports. So why not sort them automatically? We can do it with trivago/prettier-plugin-sort-imports.

npm install --save-dev @trivago/prettier-plugin-sort-imports
npm install --save-dev @trivago/prettier-plugin-sort-imports

Add plugin configuration to the Prettier config .prettierrc.js:

module.exports = {
// ... prettier config here

plugins: ['@trivago/prettier-plugin-sort-imports'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderCaseInsensitive: true,
importOrder: [
'<THIRD_PARTY_MODULES>',
// '^(.*)/components/(.*)$', // Add any folders you want to be separate
'^src/(.*)$',
'^(.*)/(?!generated)(.*)/(.*)$', // Everything not generated
'^(.*)/generated/(.*)$', // Everything generated
'^[./]' // Absolute path imports
]
}
module.exports = {
// ... prettier config here

plugins: ['@trivago/prettier-plugin-sort-imports'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderCaseInsensitive: true,
importOrder: [
'<THIRD_PARTY_MODULES>',
// '^(.*)/components/(.*)$', // Add any folders you want to be separate
'^src/(.*)$',
'^(.*)/(?!generated)(.*)/(.*)$', // Everything not generated
'^(.*)/generated/(.*)$', // Everything generated
'^[./]' // Absolute path imports
]
}

Add a new prettier script to our package.json.

...
"scripts": {
...
"prettier": "prettier --plugin '@trivago/prettier-plugin-sort-imports' --log-level error --no-error-on-unmatched-pattern --write src/**/*.{js,ts,jsx,tsx}"
},
...
...
"scripts": {
...
"prettier": "prettier --plugin '@trivago/prettier-plugin-sort-imports' --log-level error --no-error-on-unmatched-pattern --write src/**/*.{js,ts,jsx,tsx}"
},
...

Changelog

We can use the standard-version tool to generate a changelog, bump the version of the app, and create a new tag automatically.

How It Works:

npm install --save-dev standard-version
npm install --save-dev standard-version

Create the .versionrc.js config:

module.exports = {
types: [
{ type: 'feat', section: 'New features' },
{ type: 'fix', section: 'Bug fixes' },
{ type: 'change', section: 'Changes' },
{ type: 'chore', hidden: true },
{ type: 'docs', hidden: true },
{ type: 'style', hidden: true },
{ type: 'perf', hidden: true },
{ type: 'test', hidden: true }
]
}
module.exports = {
types: [
{ type: 'feat', section: 'New features' },
{ type: 'fix', section: 'Bug fixes' },
{ type: 'change', section: 'Changes' },
{ type: 'chore', hidden: true },
{ type: 'docs', hidden: true },
{ type: 'style', hidden: true },
{ type: 'perf', hidden: true },
{ type: 'test', hidden: true }
]
}

We enabled the feat, fix, and change commit types. If you want to enable the other commit types, you can remove the hidden boolean and replace it with the section string, and provide a title.

Add new release script to package.json:

...
"scripts": {
...
"release": "standard-version"
},
...
...
"scripts": {
...
"release": "standard-version"
},
...

Now we can run the npm run release command to get the changelog.

Husky

Husky improves your commits and more 🐶 woof!

We will use Husky to check if our commit messages follow the conventional commits rules, run the lint check, and format staged code with Prettier and ESLint.

npm install --save-dev husky
npm install --save-dev @commitlint/config-conventional @commitlint/cli
npm install --save-dev lint-staged
npm install --save-dev husky
npm install --save-dev @commitlint/config-conventional @commitlint/cli
npm install --save-dev lint-staged

Create a config for commitlint with commitlint.config.js file:

module.exports = {
extends: ['@commitlint/config-conventional']
}
module.exports = {
extends: ['@commitlint/config-conventional']
}

Setup lint-staged with package.json > lint-staged configuration:

...
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"eslint './src/**/*{js,ts,jsx,tsx}' --fix",
"prettier --write './src/**/*{js,ts,jsx,tsx}'"
]
},
...
...
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"eslint './src/**/*{js,ts,jsx,tsx}' --fix",
"prettier --write './src/**/*{js,ts,jsx,tsx}'"
]
},
...

We can add the "no-console" rule to the eslint.config.js to check that no console.logs are committed:

module.exports = defineConfig([
...
/* for lint-staged */
{
globals: {
__dirname: true
},
rules: {
'no-console': 'error'
}
}
...
])

module.exports = defineConfig([
...
/* for lint-staged */
{
globals: {
__dirname: true
},
rules: {
'no-console': 'error'
}
}
...
])

Configure Husky with:

npx husky init
npx husky init

The init command simplifies setting up Husky in a project. It creates a pre-commit script in .husky/ and updates the prepare script in package.json.

To add a pre-commit hook we need to replace everything inside the .husky/pre-commit file with:

npx --no-install lint-staged
npx --no-install lint-staged

To add a commit message hook we need to create the .husky/commit-msg file with:

npm run lint && npx --no-install commitlint --edit "$1"
npm run lint && npx --no-install commitlint --edit "$1"

SafeAreaContext

react-native-safe-area-context provides a flexible API for accessing device-safe area inset information.

npx expo install react-native-safe-area-context
npx expo install react-native-safe-area-context

Wrap your App.tsx -> App component with SafeAreaProvider:

import { SafeAreaProvider } from 'react-native-safe-area-context'

export default function App() {
return <SafeAreaProvider>...</SafeAreaProvider>
}
import { SafeAreaProvider } from 'react-native-safe-area-context'

export default function App() {
return <SafeAreaProvider>...</SafeAreaProvider>
}

And now we can use the SafeAreaView component.

SafeAreaView is a regular View component with the safe area insets applied as padding or margin.

Test with Jest and React Native Testing Library

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.

npx expo install jest-expo jest @types/jest --dev
npx expo install jest-expo jest @types/jest --dev

Update the package.json to include:

"scripts": {
...
"test": "jest"
}
"scripts": {
...
"test": "jest"
}

Jest has a number of globally-available functions, so we need to introduce these functions to ESLint with eslint-plugin-jest.

npm install --save-dev eslint-plugin-jest
npm install --save-dev eslint-plugin-jest

Add 'jest' to the plugins section of the eslint.config.js configuration file. We can omit the eslint-plugin- prefix:

module.exports = defineConfig([
...
{
plugins: ['jest']
}
])
module.exports = defineConfig([
...
{
plugins: ['jest']
}
])

The React Native Testing Library helps you to write better tests with less effort and encourages good testing practices.

npm install --save-dev @testing-library/react-native
npm install --save-dev @testing-library/dom
npm install --save-dev @testing-library/react-native
npm install --save-dev @testing-library/dom

Now it is time to add the jest.config.js configuration file:

/** @type {import('jest').Config} */
const path = require('path')

const config = {
preset: 'jest-expo',
setupFilesAfterEnv: [path.join(__dirname, 'setup-testing.js')],
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)'
],
moduleDirectories: ['node_modules', '<rootDir>']
}

module.exports = config
/** @type {import('jest').Config} */
const path = require('path')

const config = {
preset: 'jest-expo',
setupFilesAfterEnv: [path.join(__dirname, 'setup-testing.js')],
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)'
],
moduleDirectories: ['node_modules', '<rootDir>']
}

module.exports = config

Let's also add eslint-plugin-testing-library and eslint-plugin-jest-dom. Both are ESLint plugins that help to follow best practices and anticipate common mistakes when writing tests with Testing Library. For more info, see the excellent Common mistakes with React Testing Library article by Kent C. Dodds. 🤓

npm install --save-dev eslint-plugin-testing-library
npm install --save-dev eslint-plugin-jest-dom
npm install --save-dev eslint-plugin-testing-library
npm install --save-dev eslint-plugin-jest-dom

Add 'testing-library' to the plugins section and 'plugin:testing-library/react' to the extends section of our eslint.config.js configuration file:

module.exports = defineConfig([
...
{
extends: ['plugin:testing-library/react', 'plugin:jest-dom/recommended'],
plugins: ['testing-library']
}
])
module.exports = defineConfig([
...
{
extends: ['plugin:testing-library/react', 'plugin:jest-dom/recommended'],
plugins: ['testing-library']
}
])

Now we can write our first test to the src/App.test.tsx file:

import { render, screen } from '@testing-library/react-native'

import App from 'src/App'

describe('App', () => {
it('should mount without errors', () => {
expect(() => render(<App />)).not.toThrow()
})

it('should unmount without errors', () => {
render(<App />)
expect(() => screen.unmount()).not.toThrow()
})
})
import { render, screen } from '@testing-library/react-native'

import App from 'src/App'

describe('App', () => {
it('should mount without errors', () => {
expect(() => render(<App />)).not.toThrow()
})

it('should unmount without errors', () => {
render(<App />)
expect(() => screen.unmount()).not.toThrow()
})
})

And run it with the npm run test command.


Please fork the repo and add your favorite tools. 💾

Happy hacking! 🙌🏻

Credits

Photo by Bill Jelen on Unsplash.