Ziqi (Katrina) Ding

Back

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"
json

Last tested: 21 May 2025. Newer versions usually work with minor tweaks; if you’ve tested a newer one, let me know.

Official references:

1. Create the project#

  1. Create a new RN project with the latest version:

    npx @react-native-community/cli@latest init <project-name>
    sh

    To pin a specific version, pass --version:

    npx @react-native-community/cli@X.XX.X init <project-name> --version X.XX.X
    sh
  2. Follow the prompts in the terminal. Run the app once on iOS and Android to confirm the bare template works before changing anything.

  3. Switch to yarn. The template installs with npm, so delete package-lock.json, confirm your node version satisfies the engines field in package.json (e.g. "node": ">=18"), then run yarn install.

  4. Create a src folder and move App.tsx into it. Update the import in index.js:

    // ✅ this 
    import App from './src/App';
    // ❌ not this 
    import App from './App';
    js
  5. Reinstall pods and start Metro with a clean cache:

    cd ios && pod install && cd ..
    watchman watch-del-all && yarn cache clean && yarn start --reset-cache
    sh
  6. 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-svg
    • yarn add -D react-native-svg-transformer
    • Replace metro.config.js with:
    const { 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);
    js
    • Update globals.d.ts (see below)
  • Path aliases (babel-plugin-module-resolver ↗), so you can write @/components/Button instead of ../../components/Button

    • yarn add -D babel-plugin-module-resolver
    • Update babel.config.js and tsconfig.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',
        ],
      },
    ],
  ],
};
js

tsconfig.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__/**/*"]
}
json

globals.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
}
ts

2.2 Formatting and linting#

  • Sorted imports (@ianvs/prettier-plugin-sort-imports ↗)

    • yarn add -D @ianvs/prettier-plugin-sort-imports
    • Rename .prettierrc.js to .prettierrc.json and replace its content with:
    {
      "singleQuote": true,
      "plugins": ["@ianvs/prettier-plugin-sort-imports"],
      "importOrder": [
        "<BUILTIN_MODULES>",
        "",
        "<THIRD_PARTY_MODULES>",
        "<TYPES>",
        "",
        "^@/",
        "^[.]",
        "<TYPES>^[.]"
      ]
    }
    json
    • Add .prettierignore so native folders are left alone:
    ios/
    android/
    plaintext
  • Prettier as an ESLint rule (eslint-plugin-prettier ↗, eslint-config-prettier ↗)

    • yarn add -D eslint-plugin-prettier eslint-config-prettier
    • Rename .eslintrc.js to .eslintrc.json and replace its content with:
    {
      "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"
          }
        }
      ]
    }
    json

2.3 Runtime dependencies#

  • Navigation (React Navigation ↗)

    • yarn add @react-navigation/native react-native-screens react-native-safe-area-context
    • yarn add @react-navigation/stack @react-navigation/bottom-tabs
    • iOS: cd ios && pod install
    • Android: add the onCreate override to MainActivity.kt so react-native-screens survives activity restarts:
    ...
    import android.os.Bundle
    
    class MainActivity : ReactActivity() {
      ...
      // react-native-screens
      // https://reactnavigation.org/docs/getting-started/
      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(null)
      }
    }
    kotlin
  • Animations (React Native Reanimated ↗)

    • yarn add react-native-reanimated
    • Add the plugin to babel.config.js. It must be the last plugin:
    module.exports = {
      plugins: [
        ...
        'react-native-reanimated/plugin', // has to be listed last
      ],
    };
    js
    • Wrap the Metro config in metro.config.js:
    ...
    const { wrapWithReanimatedMetroConfig } = require('react-native-reanimated/metro-config');
    
    const config = {
      ...
    };
    
    module.exports = wrapWithReanimatedMetroConfig(mergeConfig(defaultConfig, config));
    js
    • iOS: cd ios && pod install
  • Gestures (React Native Gesture Handler ↗)

    • yarn add react-native-gesture-handler
    • Import it at the very top of index.js and wrap the app in GestureHandlerRootView (see the starter code below)
  • Toast-style alerts (react-native-dropdownalert ↗)

    • yarn add react-native-dropdownalert
    • I keep a small Dropdown wrapper component in src/helpers/ that holds the ref and exposes showSuccess / showError helpers, then mount it once in App.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;
tsx

index.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);
js

3. Optional extras#

AWS Amplify#

If the backend is on Amplify:

  1. Install:

    yarn add aws-amplify @aws-amplify/react-native \
      @react-native-community/netinfo \
      @react-native-async-storage/async-storage \
      react-native-get-random-values
    sh
  2. cd ios && pod install

  3. Add import 'react-native-get-random-values'; to the top of index.js, right after the gesture handler import.

  4. Add aws-config.ts plus one aws-exports.<env>.ts per environment (e.g. staging, prod) to src/.

  5. Keep the exports out of git:

    # Amplify
    **/aws-exports*.ts
    plaintext

For MFA codes, react-native-confirmation-code-field ↗ pairs well with Amplify auth.

Libraries I reach for often#

Tips#

  • Run pod install after every native dependency, not just at the end. It makes it obvious which package broke the iOS build.
  • After changing babel.config.js or metro.config.js, restart Metro with yarn start --reset-cache or 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#

Set Up a React Native Project From Scratch - My Starter Checklist
https://katrina-ziqi-ding.com/blog/set-up-rn-project
Author Ziqi (Katrina) Ding
Published at 22-08-2025