rn-to-tv-quickstart

React Native to TV: Quick Start Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rn-to-tv-quickstart" with this command: npx skills add giolaq/multi-tv-dev-power/giolaq-multi-tv-dev-power-rn-to-tv-quickstart

React Native to TV: Quick Start Guide

You know React Native. Here's what's different for TV.

Official Docs:

The Big 5 Differences from Mobile

  1. No Touch - Everything is Remote/D-pad

On mobile: Users tap buttons On TV: Users navigate with D-pad (up/down/left/right) + select

// Mobile: TouchableOpacity, Pressable just work // TV: You need spatial navigation

import { SpatialNavigationNode } from 'react-tv-space-navigation';

const TVButton = ({ onPress, children }) => { return ( <SpatialNavigationNode onSelect={onPress}> {({ isFocused }) => ( <View style={[styles.button, isFocused && styles.focused]}> {children} </View> )} </SpatialNavigationNode> ); };

Key library: react-tv-space-navigation

Important: v6.0.0+ uses a different API than v5.x:

  • v6: <SpatialNavigationRoot> wrapper + <SpatialNavigationNode> components

  • v5: SpatialNavigation.init()

  • useFocusable() hook
  1. Focus is King

Every interactive element MUST have a visible focus state. Users can't tap what they can't see.

const styles = StyleSheet.create({ button: { padding: 16, backgroundColor: '#333', }, // CRITICAL: Always show what's focused focused: { backgroundColor: '#555', borderWidth: 3, borderColor: '#fff', transform: [{ scale: 1.05 }], }, });

Focus Indicators (Use at least 2):

  • ✅ Border (3px+ white/colored)

  • ✅ Scale (1.05x - 1.1x)

  • ✅ Background color change

  • ✅ Shadow/glow effect

  • ✅ Opacity change

Testing Focus:

  • Navigate with D-pad/arrow keys

  • Focused element should be immediately obvious

  • Test in a dark room (10-foot viewing distance)

  • All interactive elements must be reachable button: { padding: 16, backgroundColor: '#333', }, // CRITICAL: Always show what's focused focused: { backgroundColor: '#555', borderWidth: 3, borderColor: '#fff', transform: [{ scale: 1.05 }], }, });

3. 10-Foot UI (Bigger Everything)

Users sit 10 feet away. What works on mobile is too small.

ElementMobileTV
Body text14-16px24-32px
Buttons44px height60-80px height
Touch targets44x44px80x80px+
Margins16px48px+

4. Safe Zones (TV Overscan)

TVs crop edges. Keep content away from borders.

const safeZones = {
  horizontal: 48,  // Left/right padding
  vertical: 27,    // Top/bottom padding
};

5. Landscape Only

TV apps are always landscape. Configure in app.json:

{ "expo": { "orientation": "landscape" } }

Platform Quick Reference

Platform
Technology
Remote Events

Android TV
Expo + react-native-tvos
KeyEvent

Apple TV
Expo + react-native-tvos
TVEventHandler

Fire TV FOS
Expo + react-native-tvos
KeyEvent

Fire TV Vega
Amazon Vega SDK
Kepler TVEventHandler

Web TV
React Native Web
Keyboard events

Prerequisites

For Android TV / Fire TV (Fire OS)

- Node.js LTS (macOS or Linux)

- Android Studio Iguana or later

- Android SDK API 31+ with TV system image

- Android TV emulator configured

For Apple TV (tvOS)

- Node.js LTS on macOS

- Xcode 16+

- tvOS SDK 17+ (install via xcodebuild -downloadAllPlatforms
)

For Fire TV Vega OS

- Amazon Vega SDK installed (see Vega section below)

Complete Project Setup

This guide creates a production-ready monorepo structure supporting all TV platforms.

Claude will ask for your app name and package ID when setting up.

Step 1: Create Monorepo Structure

# Create project root
mkdir &#x3C;PROJECT_NAME> &#x26;&#x26; cd &#x3C;PROJECT_NAME>

# Initialize Yarn
yarn init -y
yarn set version stable

# Create directory structure
mkdir -p apps packages/shared-ui/src

Step 2: Configure Root package.json

Create package.json
:

{
  "name": "@&#x3C;PROJECT_NAME>/monorepo",
  "version": "1.0.0",
  "private": true,
  "packageManager": "yarn@4.5.0",
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "dev": "yarn workspace @&#x3C;PROJECT_NAME>/expo-multi-tv start",
    "dev:android": "yarn workspace @&#x3C;PROJECT_NAME>/expo-multi-tv android",
    "dev:ios": "yarn workspace @&#x3C;PROJECT_NAME>/expo-multi-tv ios",
    "dev:web": "yarn workspace @&#x3C;PROJECT_NAME>/expo-multi-tv web",
    "dev:vega": "yarn workspace @&#x3C;PROJECT_NAME>/vega start",
    "build:vega": "yarn workspace @&#x3C;PROJECT_NAME>/vega build",
    "lint:all": "yarn workspaces foreach -pt run lint",
    "typecheck": "yarn workspaces foreach -pt run typecheck"
  },
  "resolutions": {
    "metro-source-map": "0.80.12"
  }
}

CRITICAL: The metro-source-map
 resolution fixes babel plugin errors in monorepos.

Step 3: Create Shared TypeScript Config

Create tsconfig.base.json
:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "CommonJS",
    "lib": ["ES2021"],
    "jsx": "react-native",
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true
  }
}

Step 4: Create Root Babel Config

Create babel.config.js
:

const path = require('path');

module.exports = function (api) {
  api.cache(true);

  let reanimatedPlugin;
  try {
    reanimatedPlugin = require.resolve('react-native-reanimated/plugin', {
      paths: [path.join(__dirname, 'apps/expo-multi-tv/node_modules')]
    });
  } catch (e) {
    reanimatedPlugin = null;
  }

  return {
    presets: ['babel-preset-expo'],
    plugins: reanimatedPlugin ? [reanimatedPlugin] : [],
  };
};

Step 5: Create Expo TV App

cd apps

# Create Expo TV project
npx create-expo-app@latest expo-multi-tv -e with-tv

cd expo-multi-tv

Update apps/expo-multi-tv/package.json
:

{
  "name": "@&#x3C;PROJECT_NAME>/expo-multi-tv",
  "dependencies": {
    "@&#x3C;PROJECT_NAME>/shared-ui": "*"
  }
}

Create apps/expo-multi-tv/metro.config.js
:

const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');

const config = getDefaultConfig(projectRoot);

config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(workspaceRoot, 'node_modules'),
];

module.exports = config;

Step 6: Add Vega App (Fire TV Vega OS)

# Create and enter vega directory first (files generate in current directory)
mkdir -p apps/vega
cd apps/vega

# Generate Vega project
vega project generate -n &#x3C;APP_NAME> --template helloWorld

npm install

Update apps/vega/package.json
:

{
  "name": "@&#x3C;PROJECT_NAME>/vega",
  "scripts": {
    "build:debug": "react-native build-kepler --build-type Debug",
    "build:release": "react-native build-kepler --build-type Release"
  },
  "dependencies": {
    "@&#x3C;PROJECT_NAME>/shared-ui": "*",
    "react-tv-space-navigation": "^6.0.0-beta1"
  }
}

CRITICAL: Update apps/vega/manifest.toml
 - Add the [processes]
 section or the app will crash on launch:

schema-version = 1

[package]
title = "My App"
version = "0.1.0"
id = "com.mycompany.myapp"

[components]
[[components.interactive]]
id = "com.mycompany.myapp.main"
runtime-module = "/com.amazon.kepler.keplerscript.runtime.loader_2@IKeplerScript_2_0"
launch-type = "singleton"
categories = ["com.amazon.category.main"]

# REQUIRED: Without this section, the app crashes immediately on launch
[processes]
[[processes.group]]
component-ids = ["com.mycompany.myapp.main"]

Create apps/vega/metro.config.js
:

const path = require('path');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');

const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');

const config = {
  watchFolders: [workspaceRoot],
  resolver: {
    nodeModulesPaths: [
      path.resolve(projectRoot, 'node_modules'),
      path.resolve(workspaceRoot, 'node_modules'),
    ],
    sourceExts: ['kepler.tsx', 'kepler.ts', 'tsx', 'ts', 'js', 'jsx', 'json'],
  },
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

Step 6b: Configure Spatial Navigation for Vega

react-tv-space-navigation
 works with Vega but requires a remote control configuration.

Create packages/shared-ui/src/app/remote-control/SupportedKeys.ts
:

export enum SupportedKeys {
  Up = 'up', Down = 'down', Left = 'left', Right = 'right',
  Enter = 'enter', Back = 'back', PlayPause = 'playPause',
  Rewind = 'rewind', FastForward = 'fastForward',
}

Create packages/shared-ui/src/app/remote-control/RemoteControlManager.kepler.ts
:

import { SupportedKeys } from './SupportedKeys';

const EVENT_MAP: Record&#x3C;string, SupportedKeys> = {
  left: SupportedKeys.Left, right: SupportedKeys.Right,
  down: SupportedKeys.Down, up: SupportedKeys.Up,
  select: SupportedKeys.Enter, back: SupportedKeys.Back,
};

class RemoteControlManager {
  private listeners = new Set&#x3C;(e: SupportedKeys) => void>();
  constructor() {
    const { TVEventHandler } = require('react-native');
    new TVEventHandler().enable(this, (_: any, e: any) => {
      if (e.eventKeyAction === 0 || e.eventKeyAction === undefined) {
        const key = EVENT_MAP[e.eventType];
        if (key) this.listeners.forEach(l => l(key));
      }
    });
  }
  addKeydownListener = (l: (e: SupportedKeys) => void) => { this.listeners.add(l); return l; };
  removeKeydownListener = (l: (e: SupportedKeys) => void) => { this.listeners.delete(l); };
}
export default new RemoteControlManager();

Create packages/shared-ui/src/app/configureRemoteControl.ts
:

import { Directions, SpatialNavigation } from 'react-tv-space-navigation';
import { SupportedKeys } from './remote-control/SupportedKeys';
import RemoteControlManager from './remote-control/RemoteControlManager';

SpatialNavigation.configureRemoteControl({
  remoteControlSubscriber: (callback) => {
    const map = {
      [SupportedKeys.Right]: Directions.RIGHT, [SupportedKeys.Left]: Directions.LEFT,
      [SupportedKeys.Up]: Directions.UP, [SupportedKeys.Down]: Directions.DOWN,
      [SupportedKeys.Enter]: Directions.ENTER,
    };
    return RemoteControlManager.addKeydownListener((k) => callback(map[k] ?? null));
  },
  remoteControlUnsubscriber: (l) => RemoteControlManager.removeKeydownListener(l),
});

Import in Vega App.tsx:

import '../../../packages/shared-ui/src/app/configureRemoteControl';

Step 7: Create Shared UI Package

Create packages/shared-ui/package.json
:

{
  "name": "@&#x3C;PROJECT_NAME>/shared-ui",
  "version": "1.0.0",
  "main": "src/index.ts",
  "types": "src/index.ts",
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  }
}

Create packages/shared-ui/tsconfig.json
:

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Create packages/shared-ui/src/index.ts
:

// Theme
export * from './theme';

// Components
export * from './components/FocusablePressable';

// Hooks
export * from './hooks/useScale';

Step 8: Create Theme System

Create packages/shared-ui/src/theme/index.ts
:

export const colors = {
  primary: '#E50914',
  background: '#141414',
  card: '#2F2F2F',
  text: '#FFFFFF',
  textSecondary: '#B3B3B3',
};

export const spacing = {
  xs: 4,
  sm: 8,
  md: 16,
  lg: 24,
  xl: 32,
  xxl: 48,
};

export const safeZones = {
  horizontal: 48,
  vertical: 27,
};

Step 9: Create useScale Hook

Create packages/shared-ui/src/hooks/useScale.ts
:

import { Dimensions } from 'react-native';

const BASE_WIDTH = 1920;
const BASE_HEIGHT = 1080;

export const useScale = () => {
  const { width, height } = Dimensions.get('window');
  const scaleWidth = width / BASE_WIDTH;
  const scaleHeight = height / BASE_HEIGHT;
  const scale = Math.min(scaleWidth, scaleHeight);

  return {
    scale: (size: number) => size * scale,
    width,
    height,
  };
};

Step 10: Create FocusablePressable Component

Create packages/shared-ui/src/components/FocusablePressable.tsx
:

import React from 'react';
import { Pressable, StyleSheet, ViewStyle } from 'react-native';
import { SpatialNavigationNode } from 'react-tv-space-navigation';

interface FocusablePressableProps {
  children: React.ReactNode;
  onPress?: () => void;
  style?: ViewStyle;
  focusedStyle?: ViewStyle;
}

export const FocusablePressable: React.FC&#x3C;FocusablePressableProps> = ({
  children,
  onPress,
  style,
  focusedStyle,
}) => {
  return (
    &#x3C;SpatialNavigationNode onSelect={onPress}>
      {({ isFocused }) => (
        &#x3C;Pressable
          onPress={onPress}
          style={[
            styles.container,
            style,
            isFocused &#x26;&#x26; styles.focused,
            isFocused &#x26;&#x26; focusedStyle,
          ]}
        >
          {children}
        &#x3C;/Pressable>
      )}
    &#x3C;/SpatialNavigationNode>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 16,
  },
  focused: {
    borderWidth: 3,
    borderColor: '#fff',
    transform: [{ scale: 1.05 }],
  },
});

Important: Pass onPress
 to both SpatialNavigationNode
 (for remote/D-pad) AND Pressable
 (for touch/click).

Focus Highlighting: The isFocused
 prop from the render function is used to apply visual feedback:

{({ isFocused }) => (
  &#x3C;Pressable
    onPress={onPress}
    style={[
      styles.container,
      style,
      isFocused &#x26;&#x26; styles.focused,  // Apply focus styles
      isFocused &#x26;&#x26; focusedStyle,    // Allow custom focus styles
    ]}
  >
    {children}
  &#x3C;/Pressable>
)}

The default focus styles provide:

- 3px white border - Clear visual boundary

- 1.05x scale - Subtle size increase

- Custom focusedStyle prop - Override with app-specific styles

Best Practices for Focus:

- Always use high-contrast colors (white/yellow on dark backgrounds)

- Combine multiple indicators (border + scale + color)

- Test on actual TV hardware (emulators may not show true visibility)

- Ensure focus is visible from 10 feet away

Step 11: Install Dependencies and Run

# From project root
cd ../..
yarn install

# Run on platforms
yarn dev:android    # Android TV / Fire TV FOS
yarn dev:ios        # Apple TV
yarn dev:web        # Web TV
yarn dev:vega       # Fire TV Vega OS

Final Project Structure

&#x3C;PROJECT_NAME>/
├── apps/
│   ├── expo-multi-tv/      # Android TV, Apple TV, Fire TV FOS, Web
│   │   ├── App.tsx         # Main app entry point
│   │   ├── app.json
│   │   ├── metro.config.js
│   │   └── package.json
│   └── vega/               # Fire TV Vega OS
│       ├── App.tsx
│       ├── metro.config.js
│       └── package.json
├── packages/
│   └── shared-ui/          # Shared components &#x26; utilities
│       ├── src/
│       │   ├── components/
│       │   ├── hooks/
│       │   ├── theme/
│       │   └── index.ts
│       ├── package.json
│       └── tsconfig.json
├── babel.config.js
├── package.json
├── tsconfig.base.json
└── yarn.lock

Platform-Specific File Resolution

Metro bundler automatically resolves platform files:

- .kepler.ts/tsx
 - Fire TV Vega OS (highest priority for Vega)

- .android.ts/tsx
 - Android TV &#x26; Fire TV Fire OS

- .ios.ts/tsx
 - Apple TV (tvOS)

- .web.ts/tsx
 - Web platforms

- .ts/tsx
 - Default/shared implementation

Example:

RemoteControlManager.android.ts  → Android TV / Fire TV FOS
RemoteControlManager.ios.ts      → Apple TV
RemoteControlManager.kepler.ts   → Fire TV Vega

Build Commands

Expo TV App (Android TV, Apple TV, Fire TV FOS, Web)

yarn dev:android    # Run on Android TV emulator
yarn dev:ios        # Run on Apple TV simulator
yarn dev:web        # Run in browser

# Production builds
npx expo build:android
npx expo build:ios
npx expo export --platform web

Vega App (Fire TV Vega OS)

yarn dev:vega       # Start Metro for Vega

# Build VPKG
vega build --arch armv7 --buildType release     # Fire TV Stick
vega build --arch aarch64 --buildType release   # Fire TV (ARM64)
vega build --arch x86_64 --buildType debug      # Virtual devices

# Deploy
vega device list
vega install --vpkg build/armv7-release/&#x3C;APP_NAME>.vpkg
vega run --packageId &#x3C;PACKAGE_ID>

Common Build Issues &#x26; Solutions

Java Version Error

Error: Android Gradle plugin requires Java 17 to run. You are currently using Java 11.

Solution: Create android/gradle.properties
:

org.gradle.java.home=/path/to/java17
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
hermesEnabled=true
android.useAndroidX=true
android.enableJetifier=true

Metro Source Map Error (Monorepo)

Error: Cannot find module 'metro-source-map/private/source-map'

Cause: Expo's metro package expects a private/
 directory that doesn't exist in metro-source-map 0.80.x

Solution: Create symlinks after yarn install
:

cd node_modules/metro-source-map
mkdir -p private/Consumer
ln -sf ../src/source-map.js private/source-map.js
for file in src/Consumer/*.js; do 
  ln -sf "../../$file" "private/Consumer/$(basename $file)"
done

Better Solution: Add to package.json
 scripts:

{
  "scripts": {
    "postinstall": "node scripts/fix-metro-source-map.js"
  }
}

Create scripts/fix-metro-source-map.js
:

const fs = require('fs');
const path = require('path');

const metroSourceMapPath = path.join(__dirname, '../node_modules/metro-source-map');
const privatePath = path.join(metroSourceMapPath, 'private');
const consumerPath = path.join(privatePath, 'Consumer');

if (!fs.existsSync(privatePath)) {
  fs.mkdirSync(privatePath, { recursive: true });
}
if (!fs.existsSync(consumerPath)) {
  fs.mkdirSync(consumerPath, { recursive: true });
}

// Create source-map.js symlink
const sourceMapSrc = path.join(metroSourceMapPath, 'src/source-map.js');
const sourceMapDest = path.join(privatePath, 'source-map.js');
if (!fs.existsSync(sourceMapDest)) {
  fs.symlinkSync(path.relative(privatePath, sourceMapSrc), sourceMapDest);
}

// Create Consumer symlinks
const consumerSrcPath = path.join(metroSourceMapPath, 'src/Consumer');
const files = fs.readdirSync(consumerSrcPath).filter(f => f.endsWith('.js'));
files.forEach(file => {
  const src = path.join(consumerSrcPath, file);
  const dest = path.join(consumerPath, file);
  if (!fs.existsSync(dest)) {
    fs.symlinkSync(path.relative(consumerPath, src), dest);
  }
});

console.log('✓ Fixed metro-source-map private paths');

App Entry Point Error (Monorepo)

Error: Unable to resolve "../../App" from "node_modules/expo/AppEntry.js"

Cause: In monorepo, Expo's default AppEntry.js looks for App.tsx at wrong path

Solution: Create index.js
 in app root:

import { registerRootComponent } from 'expo';
import App from './App';

registerRootComponent(App);

This overrides Expo's default entry point and uses the correct relative path.

react-tv-space-navigation API Error

Error: TypeError: SpatialNavigation.init is not a function (it is undefined)

Cause: react-tv-space-navigation v6.0.0+ has breaking API changes from v5.x

v5.x API (OLD - Don't use):

import { SpatialNavigation, useFocusable } from 'react-tv-space-navigation';

// Initialize
SpatialNavigation.init({ debug: true });

// Use hook
const { ref, focused } = useFocusable({ onEnterPress: onPress });

v6.0.0+ API (NEW - Use this):

import { SpatialNavigationRoot, SpatialNavigationNode } from 'react-tv-space-navigation';

// Wrap app
&#x3C;SpatialNavigationRoot>
  &#x3C;App />
&#x3C;/SpatialNavigationRoot>

// Use component with render prop
&#x3C;SpatialNavigationNode onSelect={onPress}>
  {({ isFocused }) => (
    &#x3C;View style={isFocused &#x26;&#x26; styles.focused}>
      {children}
    &#x3C;/View>
  )}
&#x3C;/SpatialNavigationNode>

Migration Steps:

- Remove SpatialNavigation.init()
 calls

- Wrap root component in &#x3C;SpatialNavigationRoot>

- Replace useFocusable()
 with &#x3C;SpatialNavigationNode>
 render prop

- Change focused
 to isFocused
 in render prop

- Change onEnterPress
 to onSelect

Hermes Configuration Missing

Error: Could not get unknown property 'hermesEnabled'

Solution: Add to android/gradle.properties
:

hermesEnabled=true

Focus Management Troubleshooting

Focus Not Visible

Problem: Can't see which element is focused

Solutions:

- Increase border width: borderWidth: 4
 or higher

- Use high-contrast colors: borderColor: '#FFFF00'
 (yellow)

- Add multiple indicators:

focused: {
  borderWidth: 4,
  borderColor: '#fff',
  transform: [{ scale: 1.1 }],
  backgroundColor: '#444',
  shadowColor: '#fff',
  shadowOffset: { width: 0, height: 0 },
  shadowOpacity: 0.8,
  shadowRadius: 10,
}

Focus Not Moving Between Elements

Problem: D-pad navigation doesn't move focus

Causes &#x26; Solutions:

- Missing SpatialNavigationRoot: Wrap entire app

- Elements not aligned: Ensure buttons are in proper layout (Row/Column)

- Check console: Look for spatial navigation warnings

- Test with multiple elements: Need at least 2 focusable items

Button Press Not Working

Problem: Focused button doesn't respond to Enter/Select

Solution: Ensure both onSelect
 AND onPress
 are set:

&#x3C;SpatialNavigationNode onSelect={handlePress}>
  {({ isFocused }) => (
    &#x3C;Pressable onPress={handlePress}>  {/* Both needed! */}

Common Mistakes (Don't Do These)

❌ Using Pressable without Focus Management

// WRONG
&#x3C;Pressable onPress={handlePress}>&#x3C;Text>Click&#x3C;/Text>&#x3C;/Pressable>

// RIGHT - Use FocusablePressable from shared-ui
&#x3C;FocusablePressable onPress={handlePress}>&#x3C;Text>Select&#x3C;/Text>&#x3C;/FocusablePressable>

❌ Forgetting Safe Zones

// WRONG - Content at edges
&#x3C;View style={{ position: 'absolute', top: 0, left: 0 }}>

// RIGHT
&#x3C;View style={{ position: 'absolute', top: 27, left: 48 }}>

❌ Small Touch Targets

// WRONG - Too small for TV
&#x3C;View style={{ width: 44, height: 44 }} />

// RIGHT
&#x3C;View style={{ width: 80, height: 80 }} />

Vega Troubleshooting

App Crashes Immediately on Launch (Black Screen)

Symptom: App installs successfully, "Successfully launched" message appears, but app immediately crashes with black screen and no error.

Cause: Missing [processes]
 section in manifest.toml

Fix: Add to apps/vega/manifest.toml
:

[processes]
[[processes.group]]
component-ids = ["com.yourcompany.yourapp.main"]

Running on Vega Virtual Device

# Start simulator
vega simulator

# Check device is ready
vega device list

# Build and run
yarn build:release  # or build:debug
vega run-app build/aarch64-release/&#x3C;APP_NAME>_aarch64.vpkg &#x3C;PACKAGE_ID>

# Check if running
vega device is-app-running --appName &#x3C;PACKAGE_ID>

Testing Your TV App

Android TV

# In Android Studio: Tools > Device Manager > Create Device > TV
yarn dev:android

Apple TV

# In Xcode: Window > Devices and Simulators > Simulators
yarn dev:ios

Web (with keyboard)

yarn dev:web
# Navigate with arrow keys, Enter to select

Physical Devices

- Android TV: Enable developer mode, connect via ADB

- Fire TV: Enable ADB debugging in Settings

- Apple TV: Connect via Xcode

Next Steps

- Add screens - Create screens in packages/shared-ui/src/screens/

- Add navigation - Set up React Navigation in shared-ui

- Add video player - Use react-native-video (Expo) or VideoView (Vega)

- Handle remote events - Create platform-specific RemoteControlManagers

- Test on real hardware - Emulators don't show real performance

Need More?

- Full knowledge base: Use the multi-tv-builder
 skill

- Apple TV issues: Use the apple-tv-troubleshooter
 skill

- Movie/TV data API: Use the tmdb-integration
 skill

Reference Implementation

For a complete working example with all features implemented:

- Sample repo: https://github.com/AmazonAppDev/react-native-multi-tv-app-sample

Use this as a reference to see advanced patterns like video playback, navigation, and remote control handling.

Android TV / Fire TV Runtime Troubleshooting

TypeError: Cannot read property 'displayName' of undefined

Symptoms:

- App builds successfully but crashes immediately

- Metro shows: ERROR TypeError: Cannot read property 'displayName' of undefined

- App returns to launcher after brief flash

Common Causes &#x26; Fixes:

- 
Wrong import in index.js (Most Common)

// ❌ WRONG - Named import when App uses default export
import { App } from './App';

// ✅ CORRECT - Default import
import App from './App';

- 
Metro cache corruption

pkill -f "expo" 2>/dev/null || true
rm -rf node_modules/.cache /tmp/metro-* /tmp/haste-map-*
npx expo start --clear

- 
react-tv-space-navigation v6 missing configuration

// Create src/configureRemoteControl.ts
import { SpatialNavigation } from 'react-tv-space-navigation';

SpatialNavigation.configureRemoteControl({
  remoteControlSubscriber: (callback) => () => {},
  remoteControlUnsubscriber: () => {},
});

Metro Bundler Connection Issues

Symptoms:

- Couldn't connect to "ws://localhost:8081/message..."

- App shows loading screen indefinitely

Fixes:

# Set up ADB reverse port forwarding
adb reverse tcp:8081 tcp:8081

# Verify emulator connection
adb devices -l

# Restart ADB if stale
adb kill-server &#x26;&#x26; adb start-server

Android TV Emulator Quick Commands

# Start emulator
emulator -avd Android_TV_720p -no-snapshot-load &#x26;

# Wait for boot
adb wait-for-device

# Force restart app
adb shell am force-stop &#x3C;package_name>
adb shell am start -n &#x3C;package_name>/.MainActivity

# Check logs for JS errors
adb logcat -d | grep -iE "(ReactNativeJS|error)" | tail -30

Expo Go vs Development Builds

Important: TV apps should use development builds, not Expo Go.

# ❌ May cause SDK version issues on TV
npx expo start

# ✅ Correct for TV development
npx expo run:android
# or
npx expo start --dev-client

Vega in Monorepo - Critical Setup

Port Forwarding Required

Vega virtual devices need reverse port forwarding to connect to Metro:

# REQUIRED before launching app
vega device start-port-forwarding --port 8081 --forward false

Without this, the app shows a black screen because it can't fetch the JS bundle.

React Version Conflicts (Expo + Vega)

If your monorepo has both Expo (React 19) and Vega (React 18), you'll get:

TypeError: Cannot read property 'ReactCurrentOwner' of undefined

Fix: Move root React packages before running Vega:

mv node_modules/react node_modules/react.bak
mv node_modules/react-native node_modules/react-native.bak

Node.js 23 Compatibility

Node 23 breaks Metro's package exports. Fix by removing exports
 field:

// Run this after yarn install
const fs = require('fs');
['metro', 'metro-source-map', 'metro-transform-worker', 'metro-runtime'].forEach(pkg => {
  const p = `node_modules/${pkg}/package.json`;
  if (fs.existsSync(p)) {
    const json = JSON.parse(fs.readFileSync(p));
    delete json.exports;
    fs.writeFileSync(p, JSON.stringify(json, null, 2));
  }
});

Complete Vega Launch Sequence

# 1. Fix Metro (Node 23 only)
node fix-metro-exports.js

# 2. Move root React (monorepo only)
mv node_modules/react node_modules/react.bak

# 3. Start Metro
cd apps/vega &#x26;&#x26; npm start

# 4. Port forwarding (new terminal)
vega device start-port-forwarding --port 8081 --forward false

# 5. Launch
vega device launch-app --appName com.yourcompany.app

# 6. Verify
vega device running-apps | grep yourapp

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

Skill Creator (Opencode)

Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize a...

Registry SourceRecently Updated
Coding

Funnel Builder

Builds complete multi-channel revenue funnels adapted to any business model. Combines proven frameworks from elite operators: Yomi Denzel's viral top-of-funn...

Registry SourceRecently Updated
Coding

macos-wechat-send

Automates sending messages on WeChat Mac by controlling the app via AppleScript and clipboard to reliably deliver text to specified contacts.

Registry SourceRecently Updated
Coding

Rednote CLI

Use when the user needs to publish, search, inspect, log into, or otherwise operate Xiaohongshu (RedNote) from the terminal with the `@skills-store/rednote`...

Registry SourceRecently Updated