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.