Handling TV User Input in a React App: Part 2

David Aragon
6 min readJan 12, 2022

In part 1, we looked at the mental model for thinking through the concept of focus in a React TV app. We then dove into how our app should route keyboard events to the focused component in the React tree.

At a high level, our system works like this:

  1. Every component in the React tree responds to keyboard events through their .navigate(key) method.
  2. The top-level component dispatches incoming keyboard events through the tree. Components pass the keyboard event down the tree until the focused component acts on it.
  3. Components use refs to track which of their children are currently focused.
  4. The process of navigating from one component to another is matter of updating which of a component’s child refs is focused.

Now, we’re ready to translate these concepts into code.

Our Base Component: <Focusable />

Let’s start by creating a base component called<Focusable/>. Any component that can receive focus (Genre, Movie) will extend this base component:

There are a few important pieces of set up in each class’s constructor (pictured below):

  1. <Movie /> creates two child refs. Later, when a keyboard event comes in, Movie will pass events down to its children through these refs.
  2. <Focusable /> creates a state object with 1 key: focusedIndex. Components will start out with no children focused. So, focusedIndex will start out as null.

Let’s now jump down to the component’s render() method. A focusable component creates its children by calling a special method defined in Focusable: this.generateChildren(). For example, when a Genre calls this.generateChildren() in its render method, that creates <Movie /> components as children of the <Genre /> component.

A few things to note here:

  1. <Genre /> renders its 3 children components (its <Movie /> components) by calling this.generateChildren() in its render() method.
  2. It passes the CTA component as an argument, so this.generateChildren() knows which components to render.

Handling Keyboard Input

Now that we have parent components wired up to their child components via refs, we’re ready to handle actual keyboard input. Let’s look at a simple example: the <Movie /> component.

Every component that handles keyboard input defines a .navigate(key) method. The argument called key is a string representing the button pressed on the keyboard (UP, DOWN, LEFT, RIGHT, SELECT, or BACK).

Let’s look at how <Movie /> handles various key arguments in its .navigate(key) method:

  1. If the key is SELECT (which comes through as ‘Enter’), the user is trying to select something inside the Movie component. In our case, this means focusing one of the Movie’s child components, a <CTA />. We do this by setting our Movie’s focusedIndex to 0, which represents its first child ref.
  1. If the key is RIGHT, the user is trying to move laterally to the Movie’s second CTA. To do this, we increment the Movie’s focusedIndex to 1.
  1. The same logic applies to the LEFT key, except instead of incrementing focusedIndex, we decrement it.

If our Movie component is able to handle the key that is passed in, we return true. Otherwise we return nothing, which is the same as returning undefined.

This is how the parent component knows whether or not its child can “handle a keyboard event.” If the child component returns true from its .navigate(key), the child is “handling it.” If it doesn’t return true, the child is saying, “I can’t handle this event.”

Great! We can now handle navigation within a single component.

What happens though when a user tries to navigate directly from one component to a non-sibling or non-child component? For example, when a user presses RIGHT to move through a list of movies, they will hit an edge and expect to focus the next Genre.

Let’s break out our trusty diagram:

“Asking” Child Refs to Handle Input

Finally, let’s understand how a component actually reaches down to its “focused child” in order to pass through a keyboard event.

Recall our main logic:

  1. Component A receives a keypress event.
  2. Should Component A delegate this event further?

— 2a) If it has a focused child that can handle this keypress, yes. Pass the keyboard event to that focused child.

— 2b) Otherwise: No more delegation. Component A handles the keyboard event itself.

How does a component know if it has a “focused child that can handle this keypress”? Let’s look at the Genre component to find out:

  1. First, a focusable component defines its child refs in its constructor. Here, Genre is creating one child ref for each of its children (which will be Movie components).
  2. When Genre’s .navigate() method gets called, Genre attempts to find its focused child ref, if it has one.

— 2a) If Genre has nothing focused, its focusedIndex will be undefined, and therefore the focusedRef variable will also be undefined.

— 2b) But if it does currently have a focusedIndex defined, then the focusedRef variable will be set to its focused child ref.

3. Finally, inside .navigate(), Genre “asks” its focused child component if it can handle the current keyboard input: focusedRef?.current.navigate?.(key).

— 3a) If that returns true, the child component handled the event.

— 3b) Otherwise, the child component is saying, “I can’t handle this keyboard input.” In this case, Genre will continue down its .navigate() method to try to handle the keyboard input itself.

Wrapping Up

We now have the nuts and bolts for a full system to intelligently route keyboard input through a React component tree.

In a nutshell:

  1. Every component in the React tree responds to keyboard events through their .navigate(key) method.
  2. The top-level component dispatches incoming keyboard events through the tree. Components pass the keyboard event down the tree until the focused component acts on it.
  3. Components use refs to track which of their children are currently focused.
  4. The process of navigating from one component to another is matter of updating which of a component’s child refs is focused.

If you’re curious to peruse the codebase, you can find it here. This type of thing is hard to fully grok from a blog post, so I encourage you to clone the repo and play with it yourself.

Play around with the app on your own, and let me know what questions you have in the comments! Thanks for reading!

--

--