Developing a cross-platform TV app with React Native
Megan Lee
Posted on March 20, 2024
Written by Emmanuel Odioko
✏️
Google launched Android TV in 2014, and big brands like Sony, Philips, and Sharp joined in soon after. These smart TVs have become popular over the years, mainly for their apps. But unlike phones or computers, TVs mostly use remote controls, not touchscreens. While this makes navigating apps a bit tricky, that's where React Native shines.
React Native is great for creating apps that not only work well on various devices but also make navigating with a TV remote easy and user-friendly. React Native stands out in helping developers design TV apps that are simple to use, no matter the device.
In this article, we'll talk about how to build a cross-platform React Native TV app with smooth, natural navigation that solves the problem of spatial navigation. We'll cover each step required to make this work and share tips to make your app user-friendly on any TV.
Here are some suggested prerequisites before diving into this tutorial:
- Basic understanding of React Native: Familiarity with React Native's fundamental concepts, such as JSX, components, state, and props
- Experience with JavaScript and ES6 syntax: A good grasp of JavaScript, particularly ES6 features like arrow functions, classes, and modules, since React Native is a JavaScript-based framework
- Familiarity with TV app development basics: Awareness of the unique challenges and considerations in TV app development, such as remote control navigation
We’re also going to use the react-tv-space-navigation library in our project. You can find the source code for our demo project on GitHub. Let’s start by introducing this library.
What is react-tv-space-navigation?
With more people wanting TV apps, learning React Native becomes increasingly relevant. Developing a cross-platform TV app with React Native is key for developers entering the expanding smart TV app market.
An essential aspect of this is mastering the use of the react-tv-space-navigation library. Trying to implement this feature from scratch would require a lot of coding to produce similar results. Using a prebuilt library ensures all desired features and customizations are readily available and easy to implement.
The react-tv-space-navigation docs describe the library’s purpose and functionality very well:
React-tv-space-navigation is a library designed to solve spatial navigation in TV apps, a challenge when users rely on remote controls rather than touchscreens. While React Native TV offers a basic solution, React-tv-space-navigation excels by being cross-platform, perfect for apps on AndroidTV, tvOS, and web TV. Built on LRUD (Left, Right, Up, Down), a UI-agnostic framework, it wraps the core spatial navigation logic into a React-friendly package. This makes it easier to develop intuitive and user-friendly TV apps that work seamlessly across different platforms.
As I mentioned in the introduction, most TVs use remote controls, not touchscreens. The react-tv-space-navigation library helps developers overcome this challenge, enabling them to create apps that are not only compatible with various TV models but also easy to navigate with a remote.
This integration is a significant aid for developers striving to make user-friendly and accessible TV apps.
Limitations of the react-tv-space-navigation library
Although react-tv-space-navigation is a great library aiming to be a cross-platform solution, achieving 100 percent compatibility across all platforms can be challenging. Most existing solutions, including those integrated within React Native TV, do not offer complete cross-platform support.
The library's documentation itself acknowledges this limitation. However, the react-tv-space-navigation team has put in some admirable effort to achieve this goal.
Despite this, react-tv-space-navigation may have a smaller community of users and contributors compared to more widely used libraries such as ReNative or Norigin Spatial Navigation. This can impact the availability of resources, tutorials, and community support for troubleshooting or implementing advanced features.
Well, that’s why I wrote this tutorial: to serve as a unique guide for you.
What is spatial scrolling?
Spatial scrolling refers to the method of navigating through a digital interface using direction-based commands, typically in a 2D space. This is often seen in environments where a traditional pointer or touchscreen is not available, like with TV remotes or game controllers.
In spatial scrolling, users move through items on the screen using directional inputs (up, down, left, right) rather than by directly selecting them.
Benefits and use cases of spatial navigation
Spatial navigation is really handy for gadgets that don't have touchscreens, like TVs and game consoles, because it lets you move around without needing to scroll. It's also great for people who might find it tough to use touch or mouse controls, such as those with mobility issues.
This type of navigation offers many benefits, including:
- Consistency in usage: Makes it easier to switch between different devices in your home entertainment setup, like from a remote to a game controller
- Speed and ease of use: Can be quicker and more straightforward than regular scrolling, especially when you're looking through things set up in grids, like photo galleries or app menus
- Simplicity: Perfect for devices that don't have many buttons, like most TV remotes
This navigation pattern has found its place in several key applications, enhancing user experience and providing seamless interaction across different platforms and devices. Some of these applications include:
- Smart TV interfaces: Spatial navigation is extensively used in Smart TV interfaces for navigating through menus, apps, and settings using a remote control
- Gaming consoles: Video game consoles use spatial navigation in their menus and sometimes in games themselves
- Web browsing on non-traditional devices: Some web browsers on TVs or game consoles use spatial navigation to allow users to scroll through web pages and select links
- Virtual reality (VR) and augmented reality (AR): In VR and AR environments, spatial navigation is used to move through virtual spaces or menus
- Kiosks and public terminals: Publicly accessible terminals like ATMs, ticket kiosks, or information stands often use button-based spatial navigation
Now, let’s begin working on our demo React Native TV app.
Installing development dependencies
To set up a React Native app, navigate to a directory of choice on your local machine, open up a command window in that directory, and enter the following bash scripts in it:
npx create-expo-app react_spatialtv
The command above creates a React Native app called react_spatialtv
. Our Expo installation provides features to run your Android application on your Android or Apple mobile devices by scanning a QR code and using the Expo mobile app. You can also execute the application on the web.
After setting up a React Native application, we will need to install the react-tv-space-navigation
dependency using the following command in the CLI:
npm install react-tv-space-navigation
To make styling our application easier, we will use NativeWind, an optimized TailwindCSS library extension for React Native. We can install this tool with the following command:
npm install nativewind
npm i --dev tailwindcss@3.3.2
Next, we will set up a Tailwind config file:
npx tailwindcss init
Open up the project directory in your preferred code editor and make the following changes to Tailwind.config.js
:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./App.{js,jsx,ts,tsx}", "./<custom directory>/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
The code above adds Tailwind support to the various file extensions in the project folder. In the root directory, open the babel.config.js
file and add the NativeWind plugin to it:
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["nativewind/babel"],
};
};
Building a spatial TV user interface
In this section, we will build the UI for the spatial TV application. This interface is similar to the movie recommendations interface found on Netflix and Amazon Prime, allowing vertical scrolling as well as a horizontal scrolling through lists.
To begin, open up the project folder in your code editor of choice. For this tutorial, I will execute my React Native app on the web. This is so I can provide larger screenshots for a better visual reference to the workings of the application.
To enable NativeWind Tailwind styles on the web interface, add the following code to the App.js
file:
import { NativeWindStyleSheet } from "nativewind";
NativeWindStyleSheet.setOutput({
default: "native",
});
Setting up the remote control
On the web interface, we would not be able to access touch controls for scrolling behavior as would be possible on mobile gadgets. As a workaround, we can set up the keyboard arrow buttons to recreate the scroll function:
// necessary application imports
import { StyleSheet, Text, View, Image } from "react-native";
import {
Directions,
SpatialNavigation,
SpatialNavigationVirtualizedGrid,
SpatialNavigationRoot,
SpatialNavigationNode,
SpatialNavigationScrollView,
} from "react-tv-space-navigation";
import { NativeWindStyleSheet } from "nativewind";
import { useCallback, useState, useEffect } from "react";
export default function App() {
// set up a callback to listen for key press events
const subscribeToRemoteControl = (callback) => {
const handleKeyPress = configureRemoteControl();
eventEmitter.on("keyDown", callback);
return handleKeyPress;
};
// handle unsubscription from key remote events
const unsubscribeFromRemoteControl = (handleKeyPress) => {
KeyEvent.removeEventListener(handleKeyPress);
};
SpatialNavigation.configureRemoteControl({
remoteControlSubscriber: subscribeToRemoteControl,
remoteControlUnsubscriber: unsubscribeFromRemoteControl,
});
SpatialNavigation.configureRemoteControl({
remoteControlSubscriber: (callback) => {
const mapping = {
ArrowRight: Directions.RIGHT,
ArrowLeft: Directions.LEFT,
ArrowUp: Directions.UP,
ArrowDown: Directions.DOWN,
Enter: Directions.ENTER,
};
const eventId = window.addEventListener("keydown", (keyEvent) => {
callback(mapping[keyEvent.code]);
});
return eventId;
},
remoteControlUnsubscriber: (eventId) => {
window.removeEventListener("keydown", eventId);
},
});
// return block
}
In the code block above:
- We created a
subscribeToRemoteControl
callback function that uses anEmitter
to listen for a key-down effect - The function
unSubscribeFromRemoteControl
takes ahandleKeyPress
param, which is the callback function used to handle key presses, and removes this callback from the list of event listeners for key events -
configureRemoteControl
defines a subscriber function that handles the application ofsubscribeToRemoteControl
andunSubscribeFromRemoteControl
to handle remote control events and stop listening to those events during a key press
Next, we mapped out a list of key events and the corresponding keyboard keys that trigger each event. When one of the defined keys is pressed, it fires the key event listening function and passes the provided callback with the mapped direction.
Creating a movie dataset
For spatial space navigation, we would require some movie data to iterate over and display on the user interface. For this, we will create an array as shown below:
const data = [
{
id: "1",
title: "Avengers: Endgame",
year: "2019",
description:
"After the devastating events of Avengers: Infinity War (2018), the universe is in ruins.",
imageUrl:
"https://upload.wikimedia.org/wikipedia/en/0/0d/Avengers_Endgame_poster.jpg",
},
{
id: "2",
title: "Avengers: Infinity War",
year: "2018",
description:
"The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.",
imageUrl:
"https://upload.wikimedia.org/wikipedia/en/4/4d/Avengers_Infinity_War_poster.jpg",
},
{
id: "3",
title: "Avengers: Age of Ultron",
year: "2015",
description:
"When Tony Stark and Bruce Banner try to jump-start a dormant peacekeeping program called Ultron, things go horribly wrong and it's up to Earth's mightiest heroes to stop the villainous Ultron from enacting his terrible plan.",
imageUrl:
"https://upload.wikimedia.org/wikipedia/en/0/0d/Avengers_Endgame_poster.jpg",
},
{
id: "4",
title: "Avengers: Infinity War",
year: "2018",
description:
"The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.",
imageUrl:
"https://upload.wikimedia.org/wikipedia/en/4/4d/Avengers_Infinity_War_poster.jpg",
},
// add more data entries
]
In the code block above, the movie object we defined contains an id
, a title
, a description
, and an imageUrl
. We would use these attributes to render unique movie cards for our spatial navigation window.
Integrating spatial navigation
In this section, we will display our movie content and integrate spatial scrolling for the movie data. In the App.js
file, we will use the react-native-tv-space-navigation
dependency to create a component that will display all movie data in the array using a grid format.
Create a new Movies
component and add the following code:
export default function App() {
//... previous functions
// state to manage side menu toggle
const [isMenuOpen, toggleMenu] = useState(false);
// detect movement to side menu
const onDirectionHandledWithoutMovement = useCallback(
(movement) => {
if (movement === "right") {
toggleMenu(false);
}
},
[toggleMenu]
);
//detect movement to virtualized grids
const onDirectionHandledWithoutMovementGrid = useCallback(
(movement) => {
if (movement === "left") {
toggleMenu(true);
}
},
[toggleMenu]
);
const Movies = () => (
<View className=" ml-20">
<Text className=" font-semibold text-xl text-slate-200">
Welcome to my marvel movies App!
</Text>
<SpatialNavigationRoot
onDirectionHandledWithoutMovement={
onDirectionHandledWithoutMovementGrid
}
>
<View className=" h-[700px] bg-[#333] p-10 rounded-lg overflow-hidden">
<SpatialNavigationScrollView
horizontal={true}
style={{ padding: 20 }}
offsetFromStart={10}
>
<SpatialNavigationVirtualizedGrid
data={data}
renderItem={renderItem}
numberOfColumns={15}
itemHeight={200}
numberOfRenderedRows={7}
numberOfRowsVisibleOnScreen={3}
onEndReachedThresholdRowsNumber={2}
rowContainerStyle={{ gap: 30 }}
scrollBehavior="stick-to-end"
/>
</SpatialNavigationScrollView>
</View>
</SpatialNavigationRoot>
</View>
);
// return block
}
In the code block above, we are using the following components imported from the React-tv-space-navigation
library:
-
SpatialNavigationRoot
-
SpatialNavigationVirtualizedGrid
-
SpatialNavigationScrollView
Here, SpatialNavigationRoot
is the root component that serves as the container for any spatial navigation node. The property onDirectionHandledWithoutMovement
assigned to the component will be used to detect changes between this container and a side menu we will add in the latter part of this article.
SpatialNavigationScrollView
enables scrolling of spatial node components in a defined direction. Finally, we used the SpatialNavigationVirtualizedGrid
to display the data
, using a component renderItem
, and also defined the properties of the grid.
To create the renderItem
component, make the following additions to the App.js
file:
export default function App() {
//...
const renderItem = ({ item }) => {
return <FocusableNode item={item} />;
};
const FocusableNode = ({ item }) => (
<SpatialNavigationNode isFocusable={true}>
{({ isFocused }) => (
<View
className={`p-3 relative border-2 rounded-md w-[180px] h-[180px] flex flex-col justify-center items-center gap-4 ${
isFocused ? " border-white" : ""
}`}
>
<View>
<Image
source={{ uri: item.imageUrl }}
className="w-[150px] h-[100px] rounded-md"
/>
<Text className={`text-white ${isFocused ? "font-bold" : ""} `}>
{item.title}
</Text>
<Text
className={`text-white ${
isFocused ? "scale-100 ease-in-out transition-all" : ""
}`}
>
{item.year}
</Text>
</View>
</View>
)}
</SpatialNavigationNode>
);
return (
<View style={styles.container}>
<Movies />
</View>
);
}
Using the SpatialNavigationVirtualizedGrid
we created individual components for every object in the data
array. Each of these components is rendered using FocusableNode
and display texts and an image.
We also have a property called isFocused
that we use to determine if the spatial node is in focus. When the value of isFocused
is true
, we change the style.
Finally, we are rendering the Movies
component within App
. Execute your React Native app using the npm run start
command. The results would be similar to the GIF below:
Adding a side menu
To create a side menu on our page, we will create and add a new component that will take an absolute position to the left of the screen and will contain spatial navigation components:
export default function App() {
// ...
const SideMenu = () => (
<SpatialNavigationRoot
onDirectionHandledWithoutMovement={onDirectionHandledWithoutMovement}
>
<SideMenuElement onSelect={() => toggleMenu(false)} />
</SpatialNavigationRoot>
);
const SideMenuElement = ({ onSelect }) => (
<View
className={`absolute z-10 left-0 h-full flex justify-center bg-slate-900 ${
isMenuOpen ? "w-24" : "w-6"
}`}
>
<SpatialNavigationNode isFocusable onSelect={onSelect}>
{({ isFocused }) => (
<View
className={` px-2 text-white py-4 ${isFocused ? "bg-white" : ""}`}
>
<Text>{isMenuOpen ? "Animations" : "A"}</Text>
</View>
)}
</SpatialNavigationNode>
<SpatialNavigationNode isFocusable onSelect={onSelect}>
{({ isFocused }) => (
<View
className={` px-2 text-white py-4 ${isFocused ? "bg-white" : ""}`}
>
<Text>{isMenuOpen ? "Movies" : "M"}</Text>
</View>
)}
</SpatialNavigationNode>
<SpatialNavigationNode isFocusable onSelect={onSelect}>
{({ isFocused }) => (
<View
className={` px-2 text-white py-4 ${isFocused ? "bg-white" : ""}`}
>
<Text>{isMenuOpen ? "BlockBusters" : "B"}</Text>
</View>
)}
</SpatialNavigationNode>
</View>
);
return (
<View style={styles.container}>
<SideMenu />
<Movies />
</View>
);
}
In the code block above, we created a SideMenu
component. We are also using the isFocused
property to change the sidebar appearance, and onDirectionHandledWithoutMovement
to detect directional movement to the side menu. In our application, we now have the following results:
Comparison with other navigation libraries
Besides react-tv-space-navigation, there are other libraries worth considering or mentioning for creating TV apps that require spatial navigation. A couple of examples include Norigin Spatial Navigation and ReNative. Here's a quick comparison:
Compared factors | react-tv-space-navigation | ReNative | Norigin Spatial Navigation |
---|---|---|---|
Target platform compatibility | Suitable for TV applications, supporting AndroidTV, tvOS, and web platforms | Suitable for mobile, web, TVs, desktops, consoles, wearables | Suitable for key navigation (directional navigation) on web-browser apps and other browser-based smart TVs and connected TVs |
Development focus | Concentrates on spatial navigation for TV applications with React Native, ensuring that navigation within TV apps feels intuitive and responsive | Supports popular frontend frameworks like React, React Native, Next.js, and Electron, and offers a unified development platform that goes beyond spatial navigation | Primarily focused on providing spatial navigation capabilities using React Hooks |
GitHub activity | Has a dedicated GitHub presence with about 97 stars | Exhibits high activity on GitHub with a larger number of stars, suggesting a strong and active community | Shows a growing level of GitHub activity with about 264 stars |
Programming Language and Framework | TypeScript | TypeScript | TypeScript |
License | Not explicitly mentioned | MIT License | MIT License |
Conclusion
Developing a cross-platform TV app with React Native is increasingly important as apps for smart TVs continue to grow in popularity and relevance. The react-tv-space-navigation library offers a comprehensive solution for navigating the unique challenges of TV app development.
This approach not only simplifies the process of creating apps for various TV platforms like AndroidTV, tvOS, and web TV but also enhances the user experience with intuitive and accessible navigation.
The use of react-tv-space-navigation demonstrates how spatial navigation can be seamlessly integrated into React Native projects. This method greatly benefits both developers and users, ensuring that navigating through TV apps with remote control is as smooth and natural as possible.
You can find the source code for the application built in this tutorial in this GitHub repository.
Further reading
For those looking to dive deeper into this subject, the React Native documentation and the react-tv-space-navigation GitHub repository are excellent resources. These platforms provide detailed insights and practical examples that can help developers effectively implement these techniques in their TV app projects.
LogRocket: Instantly recreate issues in your React Native apps
LogRocketis a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.
Posted on March 20, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.