Component Lifecycles with React Navigation in React Native

Here's another "gotcha" I came across this week as I continue to familiarise myself with React Native. Check out my previous post Transitioning from React to React Native if you're like me, and making the move from React to React Native.

Lifecycles in React Native and React Navigation

The mobile app I'm working on uses React Navigation and stack navigators. For the purposes of illustration, let's say we have 2 React components that act as pages / screens (depending if you're coming at it from the angle of web or mobile development). The first component will be called Home and the second, Details.

Come from React for web development, if I navigated from Home to the Details page,
I would expect the Home component to unmount, and the Details component to mount. If I navigate back, I'd expect the Home component to mount again at that point.

This expected component lifecycle does not behave in the same way when using stack navigation in React Native. What happens instead is as you navigate from Home to Details, Home does not unmount as it remains part of the stack, even as the Details component mounts. When you navigate back in the stack to Home, as the component was never unmounted, it just come back into focus. Note however, that at this point, the Details component does unmount, since it is no longer part of the stack. Check out the documentation if you'd like more information.

React Native screen events

The specific situation I came across was how to then refresh an API call, when a user navigates back to a previous screen that remains part of the stack. I'd normally just stick an API call within the useEffect hook and rely on this to be called upon the component mounting, but this clearly doesn't work with mobile stack navigation due to the difference in lifecycle for React Native.

Turns out there are two ways to do this, both of which relies on the concept of a screen being in focus. React Native emits screen events, whereby if a user navigates to a screen, the screen is said to be in focus. (As an aside, if a user navigates away from a screen, that screen is now in blur.) We can thus listen to these events and create our desired side effects.

Method 1: Set up a manual listener

In my example, I want to make an API call whenever a particular screen component is in focus. I can do this by setting up a manual listener. This sits within React's useEffect hook. The sequence of events is therefore:

  • The component mounts.
  • We set up a focus listener. As the screen is currently in focus, it makes an immediate API call.
  • We navigate to another screen. This first screen is now out of focus, but still mounted. The useEffect hook, and thus the focus listener is still in play.
  • We navigate back to this first screen, which means it's now in focus again. The API call is made once again.

Borrowing the example from the official docs:

function Profile({ navigation }) {
  React.useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      // Make API call here
    })

    return unsubscribe
  }, [navigation])

  return <ProfileContent />
}

Note that whenever you add a listener, you should explicitly ensure it's cleaned up upon the component unmounting. This is done with return unsubscribe in the example above.

Method 2: Use the useFocusEffect hook

Instead of manually adding a listener, we can use the useFocusEffect hook that's provided by React Navigation. This is similar to React's useEffect hook but runs on a screen being in focus instead.

In my case, I wanted to make an asynchronous API call. Here's how you can set the hook up for async functions. Expanding the example from the docs:

import { useFocusEffect } from '@react-navigation/native'

function Profile() {
  useFocusEffect(
    React.useCallback(() => {
      let isActive = true

      const fetchList = async () => {
        try {
          const tests = (await listApiService.list()).data

          if (isActive) {
            setList(test)
          }
        } catch (_) {
          setError('Something went wrong')
        }
      }

      fetchList()

      return () => {
        isActive = false
      }
    }, []),
  )

  return <ProfileContent />
}

Note that you want to wrap your side effect up in a useCallback function to ensure that your API doesn't get called unnecessarily.

Conclusion

The official docs recommends going with method 2 wherever possible. It's cleaner and is designed to integrate with React Native's component lifecycle, in addition to how React Navigation works. I also prefer the way it's written, and think it looks a lot cleaner. 🧼

Let me know what you think! Talk to me on Instagram or Twitter!

23