Video Summary and Transcription
This Talk discusses TV app development using React Native, focusing on limitations and specific considerations. The speaker demonstrates how to fetch and display random dog images using React Native hooks. They also explain how to handle focus and spatial navigation in TV apps, including using focus keys and a spatial navigation library. The Talk emphasizes the importance of optimization and performance in TV app development and suggests using useMemo and useCallback to avoid unnecessary re-renders. The speaker concludes by mentioning job opportunities at JustWatch.
1. Introduction to TV Apps Development
In this part, Michele introduces Just Watch, a TV guide for streaming services, and shares their experience in finding information about TV apps development. They discuss the limitations and specific considerations for building TV apps using React Native, including directional navigation and the use of React Native tvOS and React Native web for different platforms.
Hi there, welcome to React Native as seen on TV. I'm Michele, I'm a software developer at Just Watch. What is Just Watch in case you don't know? It's the TV guide for streaming services. So if you don't know where your favorite movie or TV show is available, or if you want to get say a notification the next time a new season for your favorite TV show comes out, you can head over to our website or use one of our many apps, including not only iOS and Android, but also TVs.
So when I joined Just Watch, I didn't know much about TV apps development, and I honestly found it kind of hard to find information about it online. Because yes, there is a section that's dedicated to it on the React Native website, but there isn't that much. So last year, when the React team came up with this blog post titled the Many Platform Vision, I was all excited and thought, oh, nice, they're finally talking about TV apps development. But it turns out that they weren't. They were really talking about React Native Windows and Mac OS and VR, but no mention of TVs. So this thing here in the animation is not a TV in case you thought that, it's actually a desktop. So bummer.
So in my talk today, I'm going to talk to you about how you can build your TV apps using React Native. What's special about TV apps development compared to mobile app development. And finally, I'm going to give you a couple of small tips and tricks that you can use to make your app perform better in a constrained environment such as the TV environment. So when you're building for TVs, the three main platforms that you're going to target are Android TV and Fire TV, based on Android, and tvOS. For these, you can use React Native tvOS, which is a fork of the main React Native repo, which is kind of unfortunate because it means that it's always going to be a little behind, but it's being updated fairly regularly. And this React Native tvOS repo gives you a number of functionality and features that you can use to build your TV apps with. This is not the whole picture though, because if you want to target LG TVs and Samsung TVs or Xboxes like we do, you will need to build your app as an HTML app. And to do so while still using the same code base that you're using for the other platforms, you can use React Native web. So talking about things that are specific to TV apps development, there is definitely directional navigation or arrow key based navigation. Your users will interact with your app only using their remote. So their options are limited there. They can only go up, down, left, right, select, go back, unless they have an LG TV, which has its own magic remote. It's really called like that, which is a pointer based interface. But other than that, they will have this arrow key based navigation. React Native tvOS gives you this TV event handler that lets you intercept these key events. And it also gives you a number of props like focusable, unfocus, unblur. These work exactly like they do on pressable on the regular React Native repo. But also some that are specific to TV app development, such as hasTVPreferredFocus, which tells the OS which component should get focused by default when rendering your view. Or the next focus down, left, up, and right props.
2. TV App Development with React Native
In this part, Michele discusses the complexity of focus in TV app development compared to mobile app development. They demonstrate a React Native app running on the Apple TV simulator and explain how to clear the code to develop TV apps with React Native. Michele also introduces a public API called Docs.io and explains how to fetch and display random dog images using React Native hooks.
And these are also used to tell the OS where focus should go, in this case, when users press the arrow keys. So compared to mobile app development, focus is a lot more complex. Usually with mobile apps, what you do is simply say, hey, if this input field is focused, please bring up the keyboard. Something like that. Not much more than that, usually.
So let's look at some code. I have a repo here, which I created using the React Native CLI, cloning this template, React Native template TypeScript TV. So if I launch Yarn iOS, this is going to bring up the Apple TV simulator and the regular React Native demo app. It looks a little weird with these colors. I'm not sure why. And aside from that, it's just your regular app. But you can see on the top right here, you can see that it's telling me when I'm pressing arrow keys, which one was pressed. This is React Native TV OS.
So let's clear everything inside here so I can show you how it is to develop TV apps with React Native. So I have my own styles, nothing fancy, just, yeah, they're my own. I'm importing them, and I'm clearing the imports. So now we have a blank canvas. Here we go. So I have this API, which is a public API called Docs.io. It's not mine. I'm just using it. I'm fetching 10 random images of dogs and giving them an ID based on their position in the array. So let's get them when the component is mounted. I'm using hooks. So dogs, set dogs. Let's have a useState, which is going to contain an array of images. We want to fetch it, so let's have a useEffect, which will actually fetch the dog images. Here we go. It's a promise. So when I get the result, I'm calling setDogs.
3. Rendering Dog Images and Handling Focus
I want to run this only once on React Native 17. TypeScript doesn't know the type of array. The function fetchDogs returns a promise. Now I'm mapping over the dogs and rendering a list of dog images. I need to handle focus and keep track of the currently focused component.
And I want to run this only once, at least on React Native 17. And this is not good because TypeScript doesn't know what type of array this is. So this is going to be the return type of the function that I just called. So fetchDogs. Here we go.
Still not good because it's a promise and I want to get the result value instead. So now it's all good, but I'm still not rendering anything. So let's render it here. Let's map over our dogs. So now I have a dog here, dog image actually. And let's render a pressable, which is exactly the same pressable that you know and love from React Native. Here we go. Pressable. Let's give it a key because it's inside of an array. And let's render an image inside, which is the image of the dog, kind of what we want it. So URI dog image. Here we go. Let's give it a style. Style. An image. And let's say it needs to take the whole area available. Here we go. So now we should have, and we do, a list of images of dogs.
And if I'm holding down the Option key and scroll here, you can see that it actually scrolls. But if I press the arrow keys instead, you can see that it still scrolls, but nothing changes because I'm not rendering anything different if something is focused. So first of all, let me make it horizontal because I like it better. And let's keep track of focus, which is something that you will do all the time in TV apps. So let's hold the state here. And let's say that this is the ID of the component that's currently focused. The ID is just going to be a string.
4. Tracking Focus and Adding Buttons
And let's keep track of the focused dog by updating the focus ID with a callback. If the dog is focused, update the style to include a blue frame. Add another button for 2D navigation. That's it!
And let's keep track of it. So whenever one of those dogs is focused, I want to update the focus ID. So how do I do that? With a callback. Let's call it on focus. And let's say use callback. I'm going to pass this ID to this function. And it's still going to be a callback. So I want another arrow here. And I want to call set focused ID, passing it the ID. Here we go. Array of dependencies. Boom.
On focus. And now let's pass it the ID of the dog. Still not changing anything render-wise. I want to update the style. So I have the styles pressable. But if it's focused, I want it to also have a frame, a blue frame around it, which is in a style that I have called styles focused. And so if this is the dog with the focus ID, so dog ID is equal to focus ID, I want to add this other style. So not only pressable, but also focused. So here we go. And now if I move around with the arrow key, you can see that everything works.
So let's add another button just below so that we have 2D navigation at least. So let's say styles.buttons. Yep. And let's add another pressable. And let's give it the same treatment that we gave the dogs. So if focused ID is equal to, let's call it button, I'm going to say styles.button, but also styles.focused. If not, only styles.button. What was that? I don't know.
5. Handling Focus and Spatial Navigation
Styles.button. We need to update focus and render the label inside the button. To enable focus navigation, set the next focus down prop using refs and the findNodeHandle function. Ensure you have the latest version of React Native tvOS for bug fixes. For React Native web, use a spatial navigation library like Norwegian Spatial Navigation.
Styles.button. Here we go. We also need to update focus. So again, on focus, and this is button we just said. And inside of it, let's render just the label. Style label. Here we go. And let's just say that this is button. So this just adds another button here, and I can navigate to it.
But if I press down here, for example, or here on any dog image that's not the first one, tvOS doesn't let me focus the button. This is because tvOS by default doesn't let you move down with focus if the component that you want to focus is not directly below the one that's currently focused. So it works here, but it doesn't work here. So how do I tell tvOS otherwise? I tell it by setting the next focus down prop. This works based on refs. So I will have to create a ref, calling it button.ref. Let's set it to null so that TypeScript doesn't complain. And let's set it on the button. So right here, ref is equal to button.ref.
And you might think that all I need to do here is just say next focus down button.ref, which would make sense, but it doesn't work because it really wants a number and not a ref. So to get a number out of a ref, you need to use React Native's findNodeHandle function. FindNodeHandle. Here we go. So it takes, of course, a current value of some ref, and it actually doesn't let me set it like this because it cannot be null, but it can be undefined. So I can just say this. So now if I save and I load and I press down here, everything works. It doesn't matter where I am.
So be sure to have the latest version of React Native tvOS because there was a bug for tvOS concerning next focus down or next focus anything only on tvOS, but it got fixed in the latest version. We looked at this for React Native tvOS, but if you're in React Native web instead, there is no native focus management because it's just an HTML page. In that case, you will need to add your own spatial navigation library, such as Norwegian Spatial Navigation, which we're using. It used to be called React Spatial Navigation.
6. Using Focus Keys and Spatial Navigation
This library is based on a unique focus key, which allows you to imperatively set focus on a specific component. You can define your own next focus props and create your own focus functions. To enable spatial navigation on React Native Web, you can use the Norwegian spatial navigation library. The useFocusable hook is essential for setting refs on focusable components. Extracting components for different platforms, such as web, is a common practice in TV app development.
It got recently renamed because they rewrote it to use hooks instead of high order components. This library is based on a unique focus key, which is a string, which is kind of better than a ref, I think, although you will need to deal with refs, but we'll see about that. And you can pass these unique focus IDs to focus keys to this set focus function, which is also provided by the library.
And that set focus function lets you imperatively set focus on a specific component, which is kind of nice. You can still define your own next focus props because those are specific to React Native tvOS, but you can create your own such as next focus down web, let's say, using the onArrowPressed function, which is provided by the library. So let's check it out.
So I have another branch here that I want to check out in which I installed React Native Web. So I have a webpack configuration. So if I run yarn web, this is going to bring up a webpack dev server. I have just a regular configuration for TypeScript. And if I look at localhost 8080, now it should be served. So focus kind of works if I press on things like I'm doing right now. But if I press the arrow keys, it doesn't do anything, as you can see, because there's no spatial navigation.
So here I did add Norwegian spatial navigation. I did the setup. There's this init function. You can pass it some values that I didn't. You need to wrap your app around this focus provider. And this focus key that I was mentioning before is actually passed by the useFocusable hook. This is the number one hook that you want to use from this library. You will use it everywhere. I was also telling you before that you will still need to deal with refs. And why? Because these useFocusable hooks will actually return a ref that you need to set on every component that can be focused by the library. So since you need to use a ref for each component, these need to be extracted to a different component, which I did in this doc component. As you can see, it's exactly the same. So something that you will do more than once, at least, while developing for TV apps is to create different flavors of components, depending on whether you're on web or not. So to do this, you do it like you do for iOS and Android. So I'm copying and pasting this class and renaming it doc.web.tsx. So this will be loaded by Webpack only and not by the regular React Native metro bundler. So let's use this doc component here.
7. Enhancing UI with Spatial Navigation
And let's add all the missing attributes. When in the web version, Spatial Navigation deals with focus. The focused prop, returned by the useFocusable hook, needs to be set on the component that can be focused. Synchronize the UI state with React Spatial Navigation or an origin Spatial Navigation using the focusSelf function. Use useEffect to handle the hasToBePreferredFocus prop. Set the ID for the focus key using the useFocusable hook. Update the type definition everywhere for TypeScript understanding.
So just like this. And let's add all the missing attributes. So image is doc.image and its focus is actually not always false, but focusedId equal to docId. So what we want to do when we're in the web version is actually let Spatial Navigation deal with focus. So this attribute will be ignored. So even if it's going to be in the interface, we're not going to use it here.
We want to have the focused prop, which is returned by the useFocusable hook. And I was telling you that this is ref-based because this returns some refs that you need to set on your component that can be focused. So that's all you need to do. Here we said we're using focused. And another thing that we need to do here is to synchronize the state of the UI with the state of React Spatial Navigation or an origin Spatial Navigation. So to do so, there's this focusSelf function that you need to call whenever your component is focused. So this keeps those in sync.
There's still one thing that we haven't done here, which is to deal with hasToBePreferredFocus because that's also React Native tvOS specific prop. So let's do a useEffect because this is only affected on mount. So if it is focused, so if it should be focused, call focusSelf. That's all there is to it. And finally, another thing that we can do is to actually decide on the ID that's used for our focus key. And to do so, you can actually set it as a parameter to the useFocusable hook. So you can say focusKey. And if you have IDs, I suggest you use them because it makes it better when you're debugging. So you need, of course, to define it. It's not alphabetically sorted, but yeah, whatever. And so this is ID. And I need to pass it. But now something that's also happening a lot when you have different flavors is you need to update the type definition everywhere because then TypeScript understands it. You can usually just extract that to a common file. That's only for types. That's what I usually do. So that's it.
8. Setting Focus and TV Performance
We need to set the component with focus by default. Extract the button to a component and add the useFocusable hook. Use the setFocus function to set focus imperatively. TVs have underpowered CPUs compared to mobile phones. TVs are not replaced as frequently as phones, so older TVs may have slower CPUs and older software versions.
And of course, now, since this is not native, we have to tell it, hey, this is the component that has focus by default. So the very first dog will have focus by default. So let's save everything. Let's see when it reloads. And now everything works. Pretty cool.
So now the button doesn't work because, again, this has not been extracted to a component and it doesn't have the hook. So to do so, we need to extract it. I have the same thing that I showed you for dogs, for a button, and that's all it does. It has its own focusable, useFocusable and ref.
So I told you about a setFocus function, and that's also returned by this useFocusable hook. We can, for example, just set focus imperatively when you press that button. So let's say that what we want to do is to onPress setFocus to the third dog, maybe. So let's say just to show something. So let's say setFocus dog2. And we need to add it. Here we go. And now onPress will do that. So onPress. So saving everything again. Here we go. I'm using the arrow keys. And if I press the button, you can see that focus goes to the third dog as we want it.
So final considerations. So TVs are very underpowered in general. Their CPUs are smaller, slower than the latest and greatest mobile phones. And that's also because they get replaced less often as phones, thankfully, because they're big devices. But usually people don't change their TV every other year like they do with phones. So if you're looking at a TV from 2017, that's not really old. And a high-end TV from 2017 might have a 1 GHz CPU and Chromium 38, which is actually from 2014.
9. Optimization and Focus in TV App Development
TV app development requires optimization due to limitations and the need for performance. Using useMemo and useCallback is crucial to avoid unnecessary re-renders and maintain performance. A trick is to only re-render components when focus changes, using React memos second argument. This functionality is similar to shouldComponentUpdate in class components. Unfortunately, there isn't enough time to explain in-depth, but JustWatch is hiring and you can reach out on Twitter or the React Summit website.
And they don't get replaced ever in some cases. So you will need to use Babel a lot, add a lot of polyfills, so it will get slower. So bringing up things from the past, so 2017 TV, so 2005 Meme. So you'll have to optimize a lot.
So remember to use useMemo, useCallback, and measurePerformance. Why am I saying this? Because lately, there's been a push against useMemo and useCallback, saying that it's kind of premature optimization. I think it's kind of the opposite, at least for TV app development, because you don't know if they actually require a lot of CPU to use useMemo and useCallback. But you definitely know that if you forget to use these, and you don't have referential integrity, so if functions keep getting changed and keep triggering re-renders all the time, this will hit performance a lot.
A trick that you can pull on TV apps specifically is to only re-render a component when focus changes. What do I mean by this? So TV app components fairly frequently are very complex, and they have a lot of props and values that you pass to them. But usually, you only want to re-render them when they're either the component that's currently focused or the one that used to be focused. So maybe you have a frame like I had in my demo app, and you want to move it. I only need to re-render those two. So how do I do that? I can use React memos second argument, which is not a secret, but not many know about it. Although this is exactly the same functionality that's provided by shouldComponentUpdate in the old lifecycle class components. It's flipped. So instead of being shouldComponentUpdate, it's kind of shouldComponentStayTheSame. So you're checking the previous props and the next props. And if focus didn't change, you don't want to re-render.
So I wish I had more time to actually go more in-depth to explain you all these tricks. But unfortunately, yeah, my 20 minutes are over. So thanks a lot for watching. JustWatch is hiring, so please reach out to me on Twitter or on any of my contacts, which are published on the React Summit website.
Comments