None REACT_NATIVE_META_QUEST_APP_INSTRUCTION

QuestExplorer – React Native App for Meta Quest 3

Expo React Native Meta Horizon

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 / questRelease variants)
  • TypeScript + New Architecture enabled
  • One-click GitHub deployment instructions

📱 Live Demo (on your Quest)

  1. Put on your Quest 3
  2. Open Expo Go (install from Horizon Store)
  3. 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

  1. Go to https://developers.meta.com/horizon/
  2. Create a new App → copy the App ID
  3. 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.isHorizonDevice for Quest-only features
  • Add expo-three for 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 of useContext when 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