Intro
A local lab is a safe, repeatable workspace for Expo development where you can test, troubleshoot, and learn without risking production work. In this guide you will:
- Install and pin the core toolchain for Expo development.
- Create a dedicated workspace for clean, isolated experiments.
- Add reproducible scripts for consistent runs across machines.
- Build a small, measurable pilot you can inspect locally.
Why it matters:
- Faster learning loop: smaller scope, quicker feedback.
- Lower risk: isolate changes from main apps and repos.
- Repeatability: scripts and pinned versions reduce drift.
Workflow Overview
The workflow below separates setup from experiments and makes each outcome easy to inspect locally.
0) Goals and Guardrails
- State what you will learn or validate (for example, Android emulator startup with a FlatList backed by a local mock API).
- Define success checks you can observe locally (for example, app reaches first render in under 10s on Android emulator; list renders 3 items from
http://10.0.2.2:4000/itemswithtimeToDataMsunder 500ms).
1) Prerequisites
- Node.js LTS (for example, 18.x or 20.x). Pin it with nvm or asdf.
- Git
- Java JDK 17 (required for Android Gradle builds)
- Android Studio with SDKs and an AVD configured (API 34 recommended)
- Xcode for iOS simulator (macOS only)
- A physical device (optional but useful for LAN testing)
Verify basics:
node -v
java -version
adb --version || echo "ADB not yet in PATH"
2) Create the Lab Workspace
Use a single directory to keep experiments tidy and versioned.
mkdir expo-lab && cd expo-lab
git init -b main
echo "18.19.0" > .nvmrc
Recommended .gitignore additions:
node_modules/
.expo/
.expo-shared/
.DS_Store
android/
ios/
.expo/*
3) Initialize Reference Apps
Create two minimal apps: one managed, one prepared for a development client.
# Managed workflow example
npx create-expo-app demo-managed --template blank
# Dev-client ready example (we will add expo-dev-client later)
npx create-expo-app demo-dev-client --template blank
4) Managed App Quick Loop
cd demo-managed
npm install
npm start # or: npx expo start
- Press
ato launch Android emulator,ito launch iOS simulator (macOS), or scan the QR code with a physical device running Expo Go. - Confirm the app loads and hot reloads UI changes.
Add basic scripts to package.json for reproducibility:
{
"scripts": {
"lab:start": "expo start",
"lab:android": "expo start --android",
"lab:ios": "expo start --ios",
"lab:clean": "rimraf node_modules .expo && npm install",
"lab:doctor": "expo doctor",
"lab:inspect:deps": "npm ls --depth=0 || true"
}
}
Install rimraf if you use the clean script on Windows:
npm install --save-dev rimraf
5) Device Matrix
Aim to validate on three targets early:
- Android emulator (AVD, API 34)
- iOS simulator (macOS, iPhone 15 Pro)
- Physical device with Expo Go on same LAN
This surfaces environment-specific issues (network host resolution, metro bundler connectivity, native module availability) while changes are still small.
6) Logging, Metrics, and Snapshots
- Use
console.logwith clear tags, for example:[LAB] mount,[LAB] fetch ok,[LAB] error: .... - Capture a timestamp at app start and after first data render to estimate cold-start time.
- Keep a
screenshots/folder in the repo for before/after proof.
7) Local Mock API
A local mock API makes experiments deterministic and network-independent.
# From expo-lab root
npm install --save-dev json-server
# Create a data file
cat > db.json <<'JSON'
{
"items": [
{ "id": 1, "name": "Alpha" },
{ "id": 2, "name": "Bravo" },
{ "id": 3, "name": "Charlie" }
]
}
JSON
# Run the server on port 4000
npx json-server --watch db.json --port 4000
Network host tips:
- iOS simulator:
http://localhost:4000works. - Android emulator: use
http://10.0.2.2:4000(10.0.2.2 maps to host localhost). - Physical device: use your machine LAN IP, e.g.,
http://192.168.1.50:4000(runifconfig | grep "inet " | grep -v 127.0.0.1to find it).
8) Example Screen with Feature Flag
In demo-managed, create a simple list with a feature flag to toggle sorting.
cd demo-managed
npm install react-native-safe-area-context
App.js:
import React, { useEffect, useState, useMemo } from 'react';
import { Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { View, Text, FlatList, Switch, StyleSheet } from 'react-native';
const HOST_ANDROID = 'http://10.0.2.2:4000';
const HOST_IOS_OR_SIM = 'http://localhost:4000';
function hostForPlatform() {
const isAndroid = Platform.OS === 'android';
return isAndroid ? HOST_ANDROID : HOST_IOS_OR_SIM;
}
export default function App() {
const [items, setItems] = useState([]);
const [sortAsc, setSortAsc] = useState(true);
const [startedAt] = useState(Date.now());
useEffect(() => {
const url = `${hostForPlatform()}/items`;
console.log('[LAB] fetching', url);
fetch(url)
.then(r => r.json())
.then(json => {
console.log('[LAB] fetch ok, count=', json.length);
setItems(json);
console.log('[LAB] timeToDataMs=', Date.now() - startedAt);
})
.catch(e => console.log('[LAB] fetch error', e));
}, [startedAt]);
const data = useMemo(() => {
const copy = [...items];
copy.sort((a, b) => sortAsc
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name));
return copy;
}, [items, sortAsc]);
return (
<SafeAreaView style={styles.root}>
<View style={styles.row}>
<Text style={styles.title}>Expo Lab List</Text>
<View style={styles.switchRow}>
<Text>Sort A-Z</Text>
<Switch value={sortAsc} onValueChange={setSortAsc} />
</View>
</View>
<FlatList
data={data}
keyExtractor={item => String(item.id)}
renderItem={({ item }) => (
<View style={styles.card}><Text>{item.name}</Text></View>
)}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
root: { flex: 1, padding: 16, backgroundColor: '#fff' },
row: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
switchRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
title: { fontSize: 20, fontWeight: '600' },
card: { padding: 12, marginVertical: 6, borderWidth: 1, borderColor: '#ddd', borderRadius: 8 }
});
Run it:
npm run lab:android # or: npm run lab:ios
Check console tags for timeToDataMs and verify the list toggles as expected.
9) Optional: Development Build for Native Modules
If you need native modules not available in Expo Go, create a development build locally.
cd ../demo-dev-client
npm install
npm install expo-dev-client
npx expo prebuild
# Build and install on Android
echo "sdk.dir=$HOME/Library/Android/sdk" >> android/local.properties || true
./android/gradlew -p android assembleDebug
npx expo run:android
# For iOS (macOS)
# npx expo run:ios
Keep the dev-client path separate from everyday managed experiments to avoid churn.
10) Repro Checklist and Scripts
Add a simple Makefile or npm scripts to standardize the loop. Example Makefile:
.PHONY: start android ios mock clean doctor
start:
cd demo-managed && npm run lab:start
android:
cd demo-managed && npm run lab:android
ios:
cd demo-managed && npm run lab:ios
mock:
npx json-server --watch db.json --port 4000
clean:
cd demo-managed && npm run lab:clean
doctor:
cd demo-managed && npm run lab:doctor
A typical session:
make mock # terminal 1: local API
make android # terminal 2: run the app on Android emulator
# Edit code, watch logs, capture screenshots
11) Map to GitLab CI/CD Locally
When you are ready to mirror parts of your lab in GitLab CI/CD, keep the following consistent with local runs:
- Pin Node and Java versions in
.gitlab-ci.yml(match.nvmrcand JDK 17). - Ensure Android SDK tools are present in the runner image (use
android-sdkDocker image or install viasdkmanager). - Reuse your lab scripts for install, lint, build, and tests.
The closer your local scripts are to your runner steps, the less rework you face later.
Local Pilot Plan
Start small and measurable so you can inspect results locally before broadening the scope.
Pilot Goal
- Render a list from a local mock API in a managed Expo app.
- Toggle sort order with a feature flag.
- Validate on Android emulator, iOS simulator, and a physical device.
Success Checks
- App launches to first render in under 10 seconds on Android emulator (API 34, cold start).
timeToDataMsconsistently under 500ms with the local mock API.- No uncaught errors in the console during normal operation.
- Screenshots saved to
screenshots/on each target.
Plan
- Environment
- Confirm Node LTS and Java 17.
- Create and boot an Android AVD (API 34), confirm Xcode simulator if on macOS.
- App
- Initialize
demo-managedand add the example screen above. - Add scripts:
lab:start,lab:android,lab:ios,lab:doctor.
- Mock API
- Run
json-serveron port 4000. - Verify host selection for each target (
localhostvs10.0.2.2vs LAN IP).
- Run and Measure
- Launch on Android emulator, observe
timeToDataMs. - Launch on iOS simulator, capture a screenshot.
- Launch on device with Expo Go using LAN IP.
- Document and Stabilize
- Record Node, npm,
json-serverversions inREADME.md. - Save commands and timings used to reproduce results.
Extensions (After Success)
- Add a second screen that writes to
AsyncStorageand reads back. - Introduce a basic test using Jest and React Native Testing Library.
- Create a development build in
demo-dev-clientif you need native modules.
Conclusion
You now have a practical local Expo lab and a first pilot that proves the loop end-to-end. The key is clarity: separate setup from experiments, keep the first pilot narrow and measurable, and codify everything with scripts so runs are repeatable.
Immediate Next Steps
- Pin versions (
.nvmrc, Java 17) and document prerequisites. - Commit your lab scripts and a short
README.mdwith the repro checklist. - Capture baseline metrics and screenshots in version control.
- Share the lab template with your team and extend incrementally.
By keeping the scope tight and the steps scripted, your Expo lab will remain fast, safe, and easy to evolve.