THE FINNTERNET

Expo Starter: Initial Setup

This Post is part of the Expo Starter series.

  1. Initial Setup (you are here)
  2. Logging & Monitoring
  3. Linting, Formatting, & Testing
  4. Navigation
  5. Push Notifications
  6. Responsive, Cross-Platforms Styling
  7. Progressive Web App (PWA)
  8. Desktop Support

Initialize Project

Create an Expo project using the blank TypeScript template.

expo init -t expo-template-blank-typescript expo-starter && cd $_

Start Expo to make sure everything is working.

yarn start

Setup Environment Configs

Store config in environment - The Twelve Factor App (Factor III)

Create Configurations

What people use for their environment names varies a little but Expo supports development and production so let's create those as well as a sample one (to show what variables are expected) and set the variable NAME in each.

// envs/.env.sample
NAME=sample
// envs/.env.development
NAME=development
// envs/.env.production
NAME=production

Load Configurations

Now that we have created our different environment configurations, we can use them in our code by utilizing dotenv and babel-plugin-inline-dotenv.

yarn add dotenv babel-plugin-inline-dotenv
// babel.config.js
module.exports = function(api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    env: {
      development: {
        plugins: [["inline-dotenv", {
          path: './envs/.env.development'
        }]]
      },
      production: {
        plugins: [["inline-dotenv", {
          path: './envs/.env.production'
        }]]
      }
    }
  };
};

Git Ignore Env Files

You should add your .env files to .gitignore so you don't commit any sensitive info (e.g. API keys, passwords, etc).

// .gitignore
// envs
envs/.env.development
envs/.env.production

Test

Okay, Let's make sure we are able to actually use the variable depending on the environment.

// App.tsx
...
export default function App() {
  return (
    <View style={styles.container}>
      <Text>process.env.NODE_ENV: {process.env.NODE_ENV}</Text>
      <Text>process.env.NAME: {process.env.NAME}</Text>
      <StatusBar style="auto" />
    </View>
  );
}
...

Run development with expo start and production with expo start --no-dev.

Organize Project

This part is up to you but I prefer to move my code to src/ and organize my projects the following way,

.expo-shared
node_modules
locales
src
├── api
├── assets
├── components
├── navigators
├── screens
├── services
├── styles
├── utilities
├── App.tsx
.gitignore
index.js    <-- We are about to create this file
app.json
babel.config.js
package.json
tsconfig.json
yarn.lock

As we add more functionality and libraries, this directory will change a bit but this serves as a good starting point.

Fix App Entry Point

Since we moved App.tsx from the root directory, which is where Expo expects our App to be, we have to create a new entry point file in the root and point to it.

// index.js
import { registerRootComponent } from 'expo';
import App from './src/App';

export default registerRootComponent(App);

I'm pretty sure this file needs to be named index.js and be in the root of the directory. I had it different before and although it worked locally, my EAS builds failed.

// package.json
{
  "main": "./index.js",
	...
}

Fix Asset Paths

Since we moved the assets, we have to also update those paths too.

// app.json
{
  "expo": {
    ...
    "icon": "./src/assets/icon.png",
    "splash": {
      "image": "./src/assets/splash.png",
      ...
    },
    ...
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./src/assets/adaptive-icon.png",
        ...
      }
    },
    "web": {
      "favicon": "./src/assets/favicon.png"
    }
  }
}

Test

Start Expo (if it isn't already), and make sure it runs in the emulator and on the web.

Create Path Aliases

Path aliases (also called module resolution depending on the tool) are a nice way to import parts of your project without having to specify an exact path. This not only saves you typing but makes it easier to restructure later if need be. Since Expo utilizes different build tools, you have to declare these in multiple places.

import { SomeUtility } from '../../utilities/MyUtility';
// Changes to
import { SomeUtility } from '@utilities/MyUtility';

Create Path Aliases for TypeScript

// tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "baseUrl": "./",
    "paths": {
      "@/api/*": ["src/api/*"],
      "@/assets/*": ["src/assets/*"],
      "@/components/*": ["src/components/*"],
      "@/locales/*": ["locales/*"],
      "@/navigators/*": ["src/navigators/*"],
      "@/screens/*": ["src/screens/*"],
      "@/services/*": ["src/services/*"],
      "@/styles/*": ["src/styles/*"],
      "@/utilities/*": ["src/utiltiies/*"],
    }
  }
}

Create Path Aliases for Expo Web

First we have to get access to the webpack configuration. Run expo customize:web and select to generate webpack.config.js.

// webpack.config.js
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
const path = require('path');

module.exports = async (env, argv) => {
  const config = await createExpoWebpackConfigAsync(env, argv);

  config.resolve.alias = {
    ...config.resolve.alias,

    '@/api': path.resolve(__dirname, 'src/api/'),
    '@/assets': path.resolve(__dirname, 'src/assets/'),
    '@/components': path.resolve(__dirname, 'src/components/'),
    '@/locales': path.resolve(__dirname, 'locales/'),
    '@/navigators': path.resolve(__dirname, 'src/navigators/'),
    '@/screens': path.resolve(__dirname, 'src/screens/'),
    '@/services': path.resolve(__dirname, 'src/services/'),
    '@/styles': path.resolve(__dirname, 'src/styles/'),
    '@/utilities': path.resolve(__dirname, 'src/utilities/'),
  };

  return config;
};

Fix Environment Variables

Generating this webpack config breaks environment variables for expo web for some reason. Luckily, to get them working again is pretty simple with dotenv-webpack.

yarn add -D dotenv-webpack
// webpack.config.js
...
const Dotenv = require('dotenv-webpack');
...
module.exports = async (env, argv) => {
  ...
  config.plugins = [
    ...config.plugins,
    new Dotenv({
      path: `./envs/.env.${process.env.NODE_ENV}`
    })
  ]
  ...
  return config;
};

Create Path Aliases for iOS/android

To handle the path aliases for iOS and Android, we will use babel-plugin-module-resolver.

yarn add -D babel-plugin-module-resolver
// .babelrc.js
{
  presets: [
    ...
  ],
  plugins: [
    [
      'module-resolver',
      {
        root: ['./'],
        alias: {
          '@/api': './src/api',
          '@/assets': './src/assets',
          '@/components': './src/components',
          '@/locales': './locales',
          '@/navigators': './src/navigators',
          '@/screens': './src/screens',
          '@/services': './src/services',
          '@/styles': './src/styles',
          '@/utilities': './src/utilities',
        },
      }
    ]
  ],
  ...
}

Create Path Aliases for Jest

We will be using Jest later so we might as well configure the path aliases for it as well.

// jest.config.js
module.exports = {
  moduleNameMapper: {
    '^@/api/(.*)': '<rootDir>/src/api/$1',
    '^@/assets/(.*)': '<rootDir>/src/assets/$1',
    '^@/components/(.*)': '<rootDir>/src/components/$1',
    '^@/locales/(.*)': '<rootDir>/locales/$1',
    '^@/navigators/(.*)': '<rootDir>/src/navigators/$1',
    '^@/screens/(.*)': '<rootDir>/src/screens/$1',
    '^@/services/(.*)': '<rootDir>/src/services/$1',
    '^@/styles/(.*)': '<rootDir>/src/styles/$1',
    '^@/utilities/(.*)': '<rootDir>/src/utilities/$1',
  },
};

Test

Okay, now that we have added the aliases to all the files we needed to, let's test it to make sure it is working on iOS, Android, and the web.

// src/utilities/test.ts
export const SomeUtility = () => { return 'Success' };
// src/App.tsx
...
import { SomeUtility } from '@utilities/test';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Path Alias: {SomeUtility()}</Text>
      <StatusBar style="auto" />
    </View>
  );
}
...

Test your app with yarn start -c (-c clears the cache). You should see "Path Alias: Success" on all the platforms.

Chris' bitmoji smiling

Hi, how's it going? I’m Chris Finn, a full-stack developer from Boston, MA. I specialize in UI and UX design, cross-platorm app development, and eating calzone. I am also a sucker for automation and security (in a past life I was a cybersecurity researcher).