QuestExplorer – React Native App for Meta Quest 3¶
A complete, ready-to-run React Native example optimized for Meta Quest 3 / Quest 3S using the official expo-horizon-core plugin (announced February 2026).
Runs as a beautiful resizable 2D floating window in VR with perfect controller/hand tracking support. Zero browser/WebXR – fully native on Horizon OS.
✨ Features¶
- Large, high-contrast VR-friendly UI (48+ dp touch targets, 24+ px fonts)
- Runtime Horizon OS detection (
expo-horizon-core) - Pre-configured window size: 1024×640 dp (perfect for Quest)
- Supports Quest 3 & Quest 3S only
- Instant hot reloading via Expo Go on Quest
- Full native builds (
questDebug/questReleasevariants) - TypeScript + New Architecture enabled
- One-click GitHub deployment instructions
📱 Live Demo (on your Quest)¶
- Put on your Quest 3
- Open Expo Go (install from Horizon Store)
- Scan the QR code below (or use the link)
→ Open in Expo Go (replace with your published link after EAS build)
🚀 Quick Start (Windows 11 Pro)¶
1. Prerequisites¶
- Node.js v20 or v22 LTS (https://nodejs.org)
- Git for Windows (https://git-scm.com)
- Meta Quest 3 with Developer Mode enabled
- Expo Go installed on your Quest from the Horizon Store
- (Optional) Meta Quest Developer Hub (MQDH)
2. Create the project¶
npx create-expo-app@latest QuestExplorer --template blank-typescript
cd QuestExplorer
npm install expo-horizon-core
3. Get your Horizon App ID¶
- Go to https://developers.meta.com/horizon/
- Create a new App → copy the App ID
- For testing you can use the demo ID:
DEMO_APP_ID
4. Update app.json (or app.config.ts)¶
{
"expo": {
"name": "QuestExplorer",
"slug": "quest-explorer",
"version": "1.0.0",
"orientation": "default",
"plugins": [
[
"expo-horizon-core",
{
"horizonAppId": "YOUR_HORIZON_APP_ID_HERE",
"defaultHeight": "640dp",
"defaultWidth": "1024dp",
"supportedDevices": "quest3|quest3s",
"disableVrHeadtracking": false
}
]
],
"newArchEnabled": true
}
}
5. Replace App.tsx¶
Copy the full App.tsx from the src/App.tsx file in this repository (or see the code block at the bottom of this README).
6. Prebuild¶
rmdir /s /q android 2>$null # Windows PowerShell
npx expo prebuild --clean
7. Test on Quest 3 (recommended)¶
npx expo start
→ Open Expo Go on Quest → Scan QR code → Instant VR app!
Hot reload works perfectly while wearing the headset.
8. Full Native Quest Build¶
npm run quest # debug build
npm run quest:release # production build
(Connect Quest via USB-C and allow debugging when prompted.)
📂 Project Structure¶
QuestExplorer/
├── app.json # Horizon plugin config
├── App.tsx # Main VR-optimized component
├── assets/
├── package.json
└── android/ # generated after prebuild
🔧 Available Scripts¶
"scripts": {
"start": "expo start",
"android": "expo run:android",
"quest": "expo run:android --variant questDebug",
"quest:release": "expo run:android --variant questRelease"
}
🛠️ Optimization Tips for Quest¶
- Use font sizes ≥24 px
- Touch targets ≥48 dp
- High contrast colors
- Check
ExpoHorizon.isHorizonDevicefor Quest-only features - Add
expo-threefor 3D immersive experiences
📤 Deploy to GitHub (from Windows)¶
git init
git add .
git commit -m "Initial commit – Quest 3 React Native app"
git branch -M main
git remote add origin https://github.com/YOURUSERNAME/quest-explorer.git
git push -u origin main
📝 Full App.tsx Code¶
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Alert, Platform } from 'react-native';
import * as ExpoHorizon from 'expo-horizon-core';
export default function App() {
const [count, setCount] = useState(0);
const isQuest = ExpoHorizon.isHorizonDevice;
const isHorizonBuild = ExpoHorizon.isHorizonBuild;
const appId = ExpoHorizon.horizonAppId ?? 'Not set';
const handlePress = () => {
setCount(c => c + 1);
if (isQuest) {
Alert.alert('Quest Detected!', 'Running natively on Meta Horizon OS 🎉');
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Quest Explorer</Text>
<Text style={styles.info}>Platform: {Platform.OS}</Text>
<Text style={styles.info}>Horizon Device: {isQuest ? '✅ Yes (Quest 3)' : 'No'}</Text>
<Text style={styles.info}>Build Type: {isHorizonBuild ? 'Quest Build' : 'Standard'}</Text>
<Text style={styles.info}>App ID: {appId}</Text>
<Text style={styles.counter}>Count: {count}</Text>
<TouchableOpacity style={styles.button} onPress={handlePress}>
<Text style={styles.buttonText}>Tap / Controller Select</Text>
</TouchableOpacity>
<Text style={styles.hint}>
Resize this window in VR • Large targets = best Quest UX
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0a0a0a', alignItems: 'center', justifyContent: 'center', padding: 40 },
title: { fontSize: 48, fontWeight: 'bold', color: '#00ffcc', marginBottom: 30 },
info: { fontSize: 24, color: '#ffffff', marginVertical: 8, textAlign: 'center' },
counter: { fontSize: 60, fontWeight: 'bold', color: '#ffffff', marginVertical: 40 },
button: { backgroundColor: '#00ffcc', paddingHorizontal: 60, paddingVertical: 24, borderRadius: 16, marginVertical: 20 },
buttonText: { fontSize: 28, fontWeight: '600', color: '#000000' },
hint: { fontSize: 20, color: '#aaaaaa', textAlign: 'center', marginTop: 40 },
});
9. Basic Unit Testing with Jest¶
To ensure your Quest app remains reliable, set up basic unit tests using Jest, which is pre-configured in Expo projects. This is especially important for VR components where state changes from Horizon OS detection need verification.
Create a new file App.test.tsx in the root directory with the following content:
import { render, fireEvent } from '@testing-library/react-native';
import App from './App';
import * as ExpoHorizon from 'expo-horizon-core';
jest.mock('expo-horizon-core', () => ({
isHorizonDevice: jest.fn(() => true),
}));
describe('App Component', () => {
it('renders correctly on Horizon device', () => {
const { getByText } = render(<App />);
expect(getByText('Welcome to QuestExplorer!')).toBeTruthy();
expect(getByText('Running on Horizon OS: Yes')).toBeTruthy();
});
it('increments counter on button press', () => {
const { getByText } = render(<App />);
const button = getByText('Press me');
fireEvent.press(button);
expect(getByText('Button pressed 1 times')).toBeTruthy();
});
});
Run tests with npm test. This simple setup tests rendering and basic interactions, scalable for more complex VR features.
🎉 Ready to build the future of VR!¶
Star this repo ⭐, fork it, and start building your own Quest 3 experiences with React Native today.
Made with ❤️ for the React Native + Meta Horizon community.
Questions? Open an issue or find me on X @faethflex
🚀 Optimizing React Native for Meta Quest 3¶
Meta Quest 3 (and 3S) runs Horizon OS (Android AOSP-based). Your React Native + Expo app runs as a resizable 2D panel floating in 3D space. To feel native and buttery-smooth, you must hit these targets:
- 72 FPS (13.7 ms per frame budget)
- Comfortable mixed-reality input (hand tracking + controllers)
- High legibility in passthrough / bright environments
1. UI/UX Design Rules (Meta Horizon OS Requirements)¶
| Element | Minimum | Recommended for Quest 3 | Why |
|---|---|---|---|
| Hit Targets | 48 dp × 48 dp | 60 dp × 60 dp (or larger) | Hand tracking + controller precision |
| Typography | 14 px | ≥24 px (body), ≥32 px (titles) | Readable while wearing headset |
| Contrast Ratio | 4.5:1 (text) | 4.5:1 text / 3:1 non-text | Avoid eye strain in passthrough |
| Colors | — | Avoid #FFFFFF pure white and #000000 pure black |
Use #DADADA max for light backgrounds |
| Spacing | 16 dp | 24–40 dp between interactive elements | Hit slop + comfort |
Code example (add to your styles):
const questStyles = StyleSheet.create({
button: {
minWidth: 240, // 60dp × 4 = 240px at base density
minHeight: 80,
paddingVertical: 24,
paddingHorizontal: 48,
borderRadius: 16,
},
title: {
fontSize: 42, // Large & bold
fontWeight: '700',
},
container: {
backgroundColor: '#111111', // Darker than pure black
padding: 40,
},
});
Pro tip: Use ExpoHorizon.isHorizonDevice to apply Quest-only styles at runtime.
2. Performance Best Practices (Expo + React Native)¶
# 1. Enable React Compiler (biggest single win on Quest)
npx react-compiler-healthcheck@latest
Then follow the official Expo guide: https://docs.expo.dev/guides/react-compiler/
// 2. Offload everything with Reanimated worklets
import { useSharedValue, withSpring } from 'react-native-reanimated';
// 3. Memoize aggressively
const MyComponent = React.memo(({ data }) => { ... });
// 4. Optimize FlatList
<FlatList
data={items}
keyExtractor={item => item.id}
getItemLayout={(data, index) => ({ length: 80, offset: 80 * index, index })}
windowSize={5}
initialNumToRender={8}
/>
Full checklist:
- Use Hermes (default in Expo)
- New Architecture enabled (
newArchEnabled: true) - Static JS + ESM imports only (no
require) TypeScript strict: true- ESLint + Expo lint rules
use hook(React 19) instead ofuseContextwhen possible
XR Animation Optimization with Reanimated¶
For buttery-smooth interactions in VR (e.g., gesture-based scaling or fading), install Reanimated:
npx expo install react-native-reanimated@~3.6.2
Add to babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['expo-router/babel', 'react-native-reanimated/plugin'],
};
};
Example in App.tsx: Use useAnimatedStyle for animated opacity on button press to minimize main-thread blocking in Quest's 90Hz+ rendering.
3. Horizon OS & Quest 3 Specific Optimizations¶
Window defaults (already in your expo-horizon-core config):JSON"defaultWidth": "1024dp", "defaultHeight": "640dp" Input: Controller & hand tracking work automatically. Add haptic feedback with expo-haptics. Forbidden APIs: No Google Play Services, no GPS (use alternatives). expo-horizon-core strips them automatically. Passthrough / Camera: Use react-native-vision-camera sparingly – keep processing under 2 ms/frame. Memory: Quest 3 has ~8 GB RAM but shared with OS. Profile early.
4. Profiling & Debugging Workflow¶
On-device: npx expo start → scan QR Shake Quest → Toggle Inspector
Chrome DevTools (JS thread): Press J in Expo terminal Profiler → "Highlight updates when components render"
Android Profiler (full native): Connect Quest via USB Open Meta Quest Developer Hub → Profiling Or use adb + Android Studio Profiler
Meta Tools: RenderDoc Meta Fork (frame analysis) OVR Metrics Tool (FPS + ms/frame)
Rule of thumb: If a function takes >2 ms → optimize or move to worklet.
5. Build & Store-Ready Optimizations¶
# Production Quest build
npm run quest:release
# Extra flags for smaller/faster APK
expo build:android --profile production --release-channel production
In app.json add:
"android": {
"minSdkVersion": 30, // Quest 3 minimum
"targetSdkVersion": 34,
"buildFeatures": { "minify": true }
}
Quick Wins You Can Add Today (5 minutes)
// In App.tsx
import { useEffect } from 'react';
import * as ExpoHorizon from 'expo-horizon-core';
useEffect(() => {
if (ExpoHorizon.isHorizonDevice) {
// Force high-performance mode
console.log('Quest 3 detected – applying optimizations');
}
}, []);
React Native New Arch for XR Performance¶
START_OF_CELL
For Quest XR apps, New Arch cuts latency via direct native/JS interop—crucial for 90Hz VR to avoid motion sickness.
Key benefits in Horizon OS:
- Fabric: Unified UI manager for smoother 3D rendering (e.g., Reanimated gestures in immersive views).
- JSI: Synchronous JS-native calls, faster than async Bridge for real-time XR input.
- TurboModules: Lazy-loaded natives, reducing APK size for Quest sideloads.
Enable in your Quest app (app.json or gradle.properties):
{
"expo": {
"android": {
"newArchEnabled": true
}
}
}
Then in App.tsx, leverage with Reanimated for XR animations:
import { useSharedValue, withTiming } from 'react-native-reanimated';
function XRComponent() {
const rotation = useSharedValue(0);
// Low-latency animation for Quest hand tracking
rotation.value = withTiming(180, { duration: 500 });
return <Animated.View style={{ transform: [{ rotateY: rotation }] }} />;
}
Pro tip: Benchmark FPS with expo-fps—aim >72Hz.
END_OF_CELL