Agent skill
rn-to-tv-quickstart
Quick start guide for experienced React Native developers transitioning to TV app development. Covers Fire OS, Android TV, Apple TV (tvOS), Vega OS, and Web TV platforms. Focuses on what's different from mobile: spatial navigation, remote controls, focus management, and 10-foot UI patterns. Skip the basics - get your first TV app running fast.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/rn-to-tv-quickstart
SKILL.md
React Native to TV: Quick Start Guide
You know React Native. Here's what's different for TV.
Official Docs:
- Expo TV: https://docs.expo.dev/guides/building-for-tv/
- Vega SDK: https://developer.amazon.com/docs/vega/latest/build-apps-overview.html
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
2. 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.
| Element | Mobile | TV |
|---------|--------|-----|
| Body text | 14-16px | 24-32px |
| Buttons | 44px height | 60-80px height |
| Touch targets | 44x44px | 80x80px+ |
| Margins | 16px | 48px+ |
### 4. Safe Zones (TV Overscan)
TVs crop edges. Keep content away from borders.
```typescript
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 <PROJECT_NAME> && cd <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": "@<PROJECT_NAME>/monorepo",
"version": "1.0.0",
"private": true,
"packageManager": "yarn@4.5.0",
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "yarn workspace @<PROJECT_NAME>/expo-multi-tv start",
"dev:android": "yarn workspace @<PROJECT_NAME>/expo-multi-tv android",
"dev:ios": "yarn workspace @<PROJECT_NAME>/expo-multi-tv ios",
"dev:web": "yarn workspace @<PROJECT_NAME>/expo-multi-tv web",
"dev:vega": "yarn workspace @<PROJECT_NAME>/vega start",
"build:vega": "yarn workspace @<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-mapresolution 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": "@<PROJECT_NAME>/expo-multi-tv",
"dependencies": {
"@<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 <APP_NAME> --template helloWorld
npm install
Update apps/vega/package.json:
{
"name": "@<PROJECT_NAME>/vega",
"scripts": {
"build:debug": "react-native build-kepler --build-type Debug",
"build:release": "react-native build-kepler --build-type Release"
},
"dependencies": {
"@<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<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<(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": "@<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<FocusablePressableProps> = ({
children,
onPress,
style,
focusedStyle,
}) => {
return (
<SpatialNavigationNode onSelect={onPress}>
{({ isFocused }) => (
<Pressable
onPress={onPress}
style={[
styles.container,
style,
isFocused && styles.focused,
isFocused && focusedStyle,
]}
>
{children}
</Pressable>
)}
</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 }) => (
<Pressable
onPress={onPress}
style={[
styles.container,
style,
isFocused && styles.focused, // Apply focus styles
isFocused && focusedStyle, // Allow custom focus styles
]}
>
{children}
</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
<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 & 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 & 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/<APP_NAME>.vpkg
vega run --packageId <PACKAGE_ID>
Common Build Issues & 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
<SpatialNavigationRoot>
<App />
</SpatialNavigationRoot>
// Use component with render prop
<SpatialNavigationNode onSelect={onPress}>
{({ isFocused }) => (
<View style={isFocused && styles.focused}>
{children}
</View>
)}
</SpatialNavigationNode>
Migration Steps:
- Remove
SpatialNavigation.init()calls - Wrap root component in
<SpatialNavigationRoot> - Replace
useFocusable()with<SpatialNavigationNode>render prop - Change
focusedtoisFocusedin render prop - Change
onEnterPresstoonSelect
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: 4or 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 & 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:
<SpatialNavigationNode onSelect={handlePress}>
{({ isFocused }) => (
<Pressable onPress={handlePress}> {/* Both needed! */}
Common Mistakes (Don't Do These)
❌ Using Pressable without Focus Management
// WRONG
<Pressable onPress={handlePress}><Text>Click</Text></Pressable>
// RIGHT - Use FocusablePressable from shared-ui
<FocusablePressable onPress={handlePress}><Text>Select</Text></FocusablePressable>
❌ Forgetting Safe Zones
// WRONG - Content at edges
<View style={{ position: 'absolute', top: 0, left: 0 }}>
// RIGHT
<View style={{ position: 'absolute', top: 27, left: 48 }}>
❌ Small Touch Targets
// WRONG - Too small for TV
<View style={{ width: 44, height: 44 }} />
// RIGHT
<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/<APP_NAME>_aarch64.vpkg <PACKAGE_ID>
# Check if running
vega device is-app-running --appName <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-builderskill - Apple TV issues: Use the
apple-tv-troubleshooterskill - Movie/TV data API: Use the
tmdb-integrationskill
Reference Implementation
For a complete working example with all features implemented:
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 & Fixes:
-
Wrong import in index.js (Most Common)
javascript// ❌ WRONG - Named import when App uses default export import { App } from './App'; // ✅ CORRECT - Default import import App from './App'; -
Metro cache corruption
bashpkill -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
typescript// 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 && adb start-server
Android TV Emulator Quick Commands
# Start emulator
emulator -avd Android_TV_720p -no-snapshot-load &
# Wait for boot
adb wait-for-device
# Force restart app
adb shell am force-stop <package_name>
adb shell am start -n <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 && 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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?