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 is designed to minimize rework by separating setup from experiments and by making each outcome easy to inspect locally.
0) Goals and guardrails
- State what you will learn or validate (for example, Android emulator setup and list rendering with a mock API).
- Define success checks you can observe locally (for example, app starts in under 10s on Android emulator and list renders from a local API).
1) Prerequisites
- Node.js LTS (for example, 18.x or 20.x). Pin it with nvm or asdf.
- Git
- Java JDK 17 (for Android builds)
- Android Studio with SDKs and an AVD configured
- Xcode for iOS simulator (macOS only)
- A physical device (optional but useful)
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 a to launch Android emulator, i to 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)
- iOS simulator (macOS)
- Physical device with Expo Go
This surfaces environment-specific issues while changes are still small.
6) Logging, metrics, and snapshots
- Use console.log with clear tags, for example:
[LAB] mount,[LAB] fetch ok,[LAB] error: .... - Capture a timestamp at app start and after first 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:4000 works.
- Android emulator: use http://10.0.2.2:4000 (10.0.2.2 maps to host localhost).
- Physical device: use your machine IP, e.g., http://192.168.1.50:4000.
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 { 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() {
// Simple heuristic: Android emulators expose generic navigator.product
// Adjust if needed per your setup.
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.
- Ensure Android SDK tools are present in the runner image.
- 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.
- timeToDataMs consistently 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, confirm Xcode simulator if on macOS.
- App
- Initialize demo-managed and add the example screen above.
- Add scripts: lab: start, lab: android, lab: ios, lab: doctor.
- Mock API
- Run json-server on port 4000.
- Verify host selection for each target (localhost vs 10.0.2.2 vs 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-server versions in README.
- Save commands and timings used to reproduce results.
Extensions (after success)
- Add a second screen that writes to local storage and reads back.
- Introduce a basic test using Jest and React Native Testing Library.
- Create a development build in demo-dev-client if 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 with 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.