React Native, as seen on TV


You might have heard of React Native's Many Platforms vision, but what about TVs? Find out about the joys and challenges of developing for 7 platforms at once with a single codebase!



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. 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 Doc CO. 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 use state, which is going to contain an array of images. We want to fetch it. So let's have a use effect, which will actually fetch the dog images. Here we go. It's a promise. So when I get the result, I'm calling set dogs. 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 fetch dogs. 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 to look 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. And let's keep track of it. So whenever one of those dogs is focused, I want to update the focused ID. So how do I do that? With a callback. Let's call it onFocus. 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 setFocusedID, passing it the ID. Here we go. Array of dependencies. Boom, onFocus. 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 stylesFocused. 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. And let's add another pressable. And let's give it the same treatment that we gave the dogs. So if focus 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. Styles.button. Here we go. We also need to update focus. So again, onFocus, and this is button we just set. 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, or 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 nextFocusDown 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 nextFocusDown 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, which 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 nextFocusDown or nextFocus anything only on tvOS, but it got fixed in the latest version. So 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. 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 setFocus function, which is also provided by the library. And that setFocus function lets you imperatively set focus on a specific component, which is kind of nice. You can still use define your own nextFocus props because those are specific to react Native tvOS, but you can create your own such as nextFocusDown web, let's say, using the onArrowPress 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. Here we go. 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. 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 TBOS specific prop. So let's do a useEffect because this is only affected on mount. And 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 focus key. 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. 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 gigahertz CPU and Chromium 38, which is actually from 2014. 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. And thanks a lot.
20 min
21 Jun, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic