Dealing with focus on React Native TVOS when going back using useFocusEffect

Sofia
4 min readJan 3, 2024

--

Data from TMDB API

If you are reading this, it is because you are curious enough or you are currently struggling with the focus issue while performing a simple and well-known “go back” action in React Navigation. As you may know, things in React Native TV-OS are sometimes not straightforward; they can get really messy since the adaptation of the framework for this platform is not at a stage where you can perform basic tasks outside of the box.

There are several threads around the internet discussing how to deal with this, and this article, as the title suggests, is about addressing this issue using a workaround I recently discovered with a lot of patience.

In summary, we will use useFocusEffect and InteractionManager to handle the focus; that's it.

Why using this and not just a simple useEffect?

This is due to the focus race condition when going back. Since this solution is not on the native side, we have to listen to the React Native lifecycle. The focus might take time to load, so you can go two ways: using a timeout or using the InteractionManager.

We will achieve this by implementing the following steps:

The focus on the last item selected on the Flatlist waits for all Interactions to finish.

What is useFocusEffect?

The useFocusEffect is a React Navigation hook in React Native that facilitates the execution of side effects, such as data fetching or event subscriptions, when a screen gains focus. It also ensures the cleanup of these effects when the screen loses focus. This hook is beneficial in mobile applications where screens frequently enter and exit focus, preventing resource wastage and unnecessary re-renders.

What is Recoil and why Recoil?

Recoil is an open-source state management library for managing the state of a React application. It is developed and maintained by Facebook, and these guys did an awesome job, Recoil is really easy to learn and is less boilerplate than Redux.

What is InteractionManager?

In React Native, InteractionManager is a module provided by the core library that allows you to manage interactions between the JavaScript thread and the native UI thread. It's particularly useful for handling tasks that are less time-sensitive or can be deferred until the app is in an idle state, thereby improving performance and responsiveness.

The main purpose of InteractionManager is to schedule tasks to be executed after interactions, such as animations or touch gestures, have completed.

So, let’s start coding…

  1. In your DetailScreen add the following:
/* code inside functional component */

const setShouldRefresh = useSetRecoilState(shouldRefreshFocus)

const setRefresh = () => {
setShouldRefresh(() => true)
}

useFocusEffect(
useCallback(() => {
//screen is focused
return () => {
//screen is going out of focus
setRefresh()
}
}, []),
)

The setRefresh() method inside the return will fire the Recoil State holding the refresh atom.

2. On your MainScreen, add the following:

/* your dependencies */
import {shouldRefreshFocus} from '@src/store/atom'
import {savedSelectedItem} from '@src/store/atom'

/* code inside functional component */

const [lastReference, setLastReference] = useState(0)

const savedItem = useRecoilValue(savedSelectedItem)

const setShouldRefresh = useSetRecoilState(shouldRefreshFocus)
const savedLastReference = useSetRecoilState(savedSelectedItem)

const handleNavigation = () => {
savedLastReference(() => last)
navigation.navigate('DetailScreen', {id: selectedId})
}

InteractionManager.runAfterInteractions(
() => refresh && setShouldRefresh(() => false),
)

lastReference will save the reference to the last item focus (for example, an ID)

When navigating to the DetailScreen, you will save the last reference.

3. Inside your Flatlist, send to your child components the following condition:

!refresh && id === saved ? true : false

When refreshed is settled to true when going back, InteractionManager will notice it when all interactions are done, firing to true the hasFocus prop:

 <FlatList
data={trending}
keyExtractor={(_, index) => `${category.category}${index}`}
renderItem={({
item: {id, title, backdrop_path, poster_path, overview},
}) => (
<Cell
id={id}
title={title}
hasFocus={!refresh && id === saved ? true : false}
imagePosterUri={poster_path}
imagePosterBackdrop={backdrop_path}
overview={overview}
handleNavigation={handleNavigation}
handleSelectedTitle={handleSelectedTitle}
/>
)}
horizontal
/>

4. Remember to add:

hasTVPreferredFocus={hasFocus}

Inside your Cell (it could be a Pressable) for example:

  <Pressable
style={styles.cellContainer}
onPress={handleNavigation}
hasTVPreferredFocus={hasFocus}
onFocus={setFocus}>
<Image
source={{
uri: `${URI}${imagePosterUri}`,
}}
style={styles.imageStyle}
resizeMode={'cover'}
/>
</Pressable>

hasTVPreferredFocus will force the focus to the element that receives true.

The Second Way

Actually, you could also use a timeout inside an useEffect:

 useEffect(() => {
let timer: NodeJS.Timeout
if (refresh) {
timer = setTimeout(() => {
setShouldRefresh(() => false)
}, 100)
}

return () => {
clearTimeout(timer)
}
}, [refresh])

I’ll post the link to the example project once I finish.

Thank you for reading!

--

--