Set Up a React Native Project From Scratch - My Starter Checklist
A repeatable checklist for a fresh React Native (no Expo) project: yarn, src folder, path aliases, SVG, env vars, Prettier/ESLint, navigation and Reanimated.
The Problem#
Every new React Native project starts the same way: the CLI gives you a bare template, then you spend the first day re-adding the same dependencies and config you had in the last project. Path aliases, SVG imports, env vars, Prettier, ESLint, navigation, Reanimated. And you always forget one.
The Solution#
This is the checklist I follow to get from init to a project I’d actually start building features in. It’s for the bare React Native CLI (no Expo). If you use Expo, create the project with their guide and jump to step 2.
The guide is confirmed working with:
"react": "19.0.0"
"react-native": "0.79.2"jsonLast tested: 21 May 2025. Newer versions usually work with minor tweaks; if you’ve tested a newer one, let me know.
Official references:
- Without Expo: https://reactnative.dev/docs/getting-started-without-a-framework ↗
- With Expo: https://reactnative.dev/docs/environment-setup ↗
1. Create the project#
-
Create a new RN project with the latest version:
shnpx @react-native-community/cli@latest init <project-name>To pin a specific version, pass
--version:
shnpx @react-native-community/cli@X.XX.X init <project-name> --version X.XX.X -
Follow the prompts in the terminal. Run the app once on iOS and Android to confirm the bare template works before changing anything.
-
Switch to yarn. The template installs with npm, so delete
package-lock.json, confirm your node version satisfies theenginesfield inpackage.json(e.g."node": ">=18"), then runyarn install. -
Create a
srcfolder and moveApp.tsxinto it. Update the import inindex.js:
js// ✅ this import App from './src/App'; // ❌ not this import App from './App'; -
Reinstall pods and start Metro with a clean cache:
shcd ios && pod install && cd .. watchman watch-del-all && yarn cache clean && yarn start --reset-cache -
Run the app on both iOS and Android again.
2. Install dependencies#
2.1 Dev dependencies#
-
Env variables (react-native-dotenv ↗)
yarn add -D react-native-dotenv- Update
globals.d.ts(see below)
-
SVG as components (react-native-svg-transformer ↗)
yarn add react-native-svgyarn add -D react-native-svg-transformer- Replace
metro.config.jswith:
jsconst { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const defaultConfig = getDefaultConfig(__dirname); const { assetExts, sourceExts } = defaultConfig.resolver; /** * Metro configuration * https://reactnative.dev/docs/metro * * @type {import('metro-config').MetroConfig} */ const config = { transformer: { babelTransformerPath: require.resolve( 'react-native-svg-transformer/react-native' ), }, resolver: { assetExts: assetExts.filter((ext) => ext !== 'svg'), sourceExts: [...sourceExts, 'svg'], }, }; module.exports = mergeConfig(defaultConfig, config);- Update
globals.d.ts(see below)
-
Path aliases (babel-plugin-module-resolver ↗), so you can write
@/components/Buttoninstead of../../components/Buttonyarn add -D babel-plugin-module-resolver- Update
babel.config.jsandtsconfig.json(see below)
-
Strip console logs in production (babel-plugin-transform-remove-console ↗)
yarn add -D babel-plugin-transform-remove-console- Update
babel.config.js(see below)
Now apply the config changes in one go.
babel.config.js:
module.exports = {
presets: ['module:@react-native/babel-preset'],
// babel-plugin-transform-remove-console
env: {
production: {
plugins: ['transform-remove-console'],
},
},
plugins: [
// react-native-dotenv
['module:react-native-dotenv'],
// babel-plugin-module-resolver
[
'module-resolver',
{
root: ['./src'],
alias: {
'@': './src',
},
extensions: [
'.js',
'.jsx',
'.ts',
'.tsx',
'.android.js',
'.android.tsx',
'.ios.js',
'.ios.tsx',
],
},
],
],
};jstsconfig.json:
{
"extends": "@react-native/typescript-config/tsconfig.json",
"compilerOptions": {
"noUnusedLocals": false,
"noUnusedParameters": false,
"useUnknownInCatchVariables": false,
"exactOptionalPropertyTypes": false,
// babel-plugin-module-resolver
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"include": ["index.js", "globals.d.ts", "src/**/*", "__tests__/**/*"]
}jsonglobals.d.ts (create it at the project root):
// react-native-svg-transformer
// https://github.com/kristerkari/react-native-svg-transformer#using-typescript
declare module '*.svg' {
import React from 'react';
import { SvgProps } from 'react-native-svg';
const content: React.FC<SvgProps>;
export default content;
}
// Image imports
declare module '*.png';
declare module '*.jpeg';
declare module '*.jpg';
// react-native-dotenv
declare module '@env' {
export const API_URL: string;
// Add the rest of your .env keys here
}ts2.2 Formatting and linting#
-
Sorted imports (@ianvs/prettier-plugin-sort-imports ↗)
yarn add -D @ianvs/prettier-plugin-sort-imports- Rename
.prettierrc.jsto.prettierrc.jsonand replace its content with:
json{ "singleQuote": true, "plugins": ["@ianvs/prettier-plugin-sort-imports"], "importOrder": [ "<BUILTIN_MODULES>", "", "<THIRD_PARTY_MODULES>", "<TYPES>", "", "^@/", "^[.]", "<TYPES>^[.]" ] }- Add
.prettierignoreso native folders are left alone:
plaintextios/ android/ -
Prettier as an ESLint rule (eslint-plugin-prettier ↗, eslint-config-prettier ↗)
yarn add -D eslint-plugin-prettier eslint-config-prettier- Rename
.eslintrc.jsto.eslintrc.jsonand replace its content with:
json{ "root": true, "extends": ["@react-native", "plugin:prettier/recommended"], "overrides": [ { "files": ["*.js", "*.jsx", "*.ts", "*.tsx"], "rules": { "dot-notation": "off", "no-console": "warn", "no-undef": "off", "react-hooks/exhaustive-deps": "warn", "react-native/no-unused-styles": "warn", "@typescript-eslint/no-unused-vars": "warn", "react/react-in-jsx-scope": "off" } } ] }
2.3 Runtime dependencies#
-
Navigation (React Navigation ↗)
yarn add @react-navigation/native react-native-screens react-native-safe-area-contextyarn add @react-navigation/stack @react-navigation/bottom-tabs- iOS:
cd ios && pod install - Android: add the
onCreateoverride toMainActivity.ktsoreact-native-screenssurvives activity restarts:
kotlin... import android.os.Bundle class MainActivity : ReactActivity() { ... // react-native-screens // https://reactnavigation.org/docs/getting-started/ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(null) } } -
Animations (React Native Reanimated ↗)
yarn add react-native-reanimated- Add the plugin to
babel.config.js. It must be the last plugin:
jsmodule.exports = { plugins: [ ... 'react-native-reanimated/plugin', // has to be listed last ], };- Wrap the Metro config in
metro.config.js:
js... const { wrapWithReanimatedMetroConfig } = require('react-native-reanimated/metro-config'); const config = { ... }; module.exports = wrapWithReanimatedMetroConfig(mergeConfig(defaultConfig, config));- iOS:
cd ios && pod install
-
Gestures (React Native Gesture Handler ↗)
yarn add react-native-gesture-handler- Import it at the very top of
index.jsand wrap the app inGestureHandlerRootView(see the starter code below)
-
Toast-style alerts (react-native-dropdownalert ↗)
yarn add react-native-dropdownalert- I keep a small
Dropdownwrapper component insrc/helpers/that holds the ref and exposesshowSuccess/showErrorhelpers, then mount it once inApp.tsx
-
Keyboard-aware forms (react-native-keyboard-aware-scroll-view ↗)
yarn add react-native-keyboard-aware-scroll-view
-
Progress indicators (react-native-progress ↗)
yarn add react-native-progress
2.4 Starter App.tsx and index.js#
With everything installed, the app shell looks like this.
src/App.tsx:
import { createNavigationContainerRef, NavigationContainer } from '@react-navigation/native';
import React from 'react';
import { StatusBar } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Dropdown } from '@/helpers';
import { AppStackParamList } from '@/navigation';
const navigationRef = createNavigationContainerRef<AppStackParamList>();
// Navigate from outside React components (e.g. from an API interceptor)
export const navigate = (name: string, params?: any) => {
if (navigationRef.isReady()) {
//@ts-ignore
navigationRef.navigate(name, params);
return true;
}
console.log('navigation not ready');
return false;
};
const App = () => {
return (
<SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<NavigationContainer ref={navigationRef}>
<Dropdown />
<StatusBar barStyle={'light-content'} />
{/* Your navigator goes here */}
</NavigationContainer>
</GestureHandlerRootView>
</SafeAreaProvider>
);
};
export default App;tsxindex.js:
/**
* @format
*/
// react-native-gesture-handler must be the first import
// https://reactnavigation.org/docs/stack-navigator#installation
import 'react-native-gesture-handler';
import { AppRegistry } from 'react-native';
import App from './src/App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);js3. Optional extras#
AWS Amplify#
If the backend is on Amplify:
-
Install:
shyarn add aws-amplify @aws-amplify/react-native \ @react-native-community/netinfo \ @react-native-async-storage/async-storage \ react-native-get-random-values -
cd ios && pod install -
Add
import 'react-native-get-random-values';to the top ofindex.js, right after the gesture handler import. -
Add
aws-config.tsplus oneaws-exports.<env>.tsper environment (e.g.staging,prod) tosrc/. -
Keep the exports out of git:
plaintext# Amplify **/aws-exports*.ts
For MFA codes, react-native-confirmation-code-field ↗ pairs well with Amplify auth.
Libraries I reach for often#
- Modal: react-native-modal ↗
- Dates and times: Luxon ↗
- Calendar: react-native-calendars ↗
- Bottom sheet: @gorhom/bottom-sheet ↗
- Validate emails and phone numbers: validator ↗
- Webview: react-native-webview ↗
- Video player: react-native-video ↗. Note: the framework needs linking manually in Xcode.
Tips#
- Run
pod installafter every native dependency, not just at the end. It makes it obvious which package broke the iOS build. - After changing
babel.config.jsormetro.config.js, restart Metro withyarn start --reset-cacheor the change silently won’t apply. - Commit right after step 1 and again after step 2. A clean “project skeleton” commit is handy to diff against when a later dependency misbehaves.
- If the iOS build breaks after adding a package, clean and reinstall before debugging further.
Summary#
One checklist turns a bare React Native template into a project with a src folder, @/ aliases, SVG and env support, consistent formatting, navigation and animations, so the next project starts on day one instead of day two.
References#
- React Native: Get started without a framework ↗
- React Native: Environment setup (Expo) ↗
- React Native: Metro configuration ↗
- react-native-dotenv ↗
- react-native-svg-transformer ↗
- babel-plugin-module-resolver ↗
- babel-plugin-transform-remove-console ↗
- @ianvs/prettier-plugin-sort-imports ↗
- eslint-plugin-prettier ↗ and eslint-config-prettier ↗
- React Navigation: Getting started ↗
- React Native Reanimated: Getting started ↗
- React Native Gesture Handler: Installation ↗
- react-native-dropdownalert ↗
- react-native-keyboard-aware-scroll-view ↗
- react-native-progress ↗
- AWS Amplify: React Native setup ↗