DEV Community

robert-metcalfe10
robert-metcalfe10

Posted on

useScreenOrientation - React Native Snippet

The Problem πŸ€”

Every day a React Native engineer has to deal with screen orientation changes and their oddities and pain points, whether that be simply changing the UI based on notches when in landscape, firing new network calls or possibly displaying/dropping the keyboard every time you rotate the device. So we need a solution to tell us whenever there has been a change to the screen orientation and, from there, we can figure out what the best course of action is.

The Naive Approach πŸ‘€

Ok. So now we know what we are trying to solve. A quick and easy approach might be to quickly compare the width of the screen to the height of the screen.

Something like this:

import { Dimensions } from 'react-native'

const isLandscape = () => Dimensions.get('window').width > Dimensions.get('window').height
Enter fullscreen mode Exit fullscreen mode

Drawbacks:

  1. It doesn't tell us actually what the exact screen orientation is, just whether we are in portrait or landscape.

  2. This solution doesn't dynamically tell us there has been a screen orientation change. Just what the screen orientation is when we called this function. For example, if I am using React Navigation and I push a new screen onto the stack, I can find out the screen orientation at the time I pushed the screen. But let's say I then rotate the device, I will still see the previous value of isLandscape unless I manually call it again.

The Better Solution πŸ’ͺ

We want to set up a hook that listens for screen orientation changes and causes a rerender anytime the screen orientation in our state is changed.

Ok. To make our lives way easier I am going to use two libraries that I think come in really handy here and allow us to not dive into native code and bridge it ourselves:

Caveat 🚨
react-native-orientation-locker seems to have a bug on Android preventing the listener from emitting events consistently(tested on React Native 0.65.1). So I am just using a basic workaround for now, until this is fixed. Unfortunately we lose the ability to know exactly which screen orientation we are in.

Step 1
Let's set up a basic hook with react-singleton-hook that we can expand on in the next step. The reason we only want a single hook at any one time, is so we don't have multiple listeners listening to changes. For example, if again you are using React Navigation and you push onto the stack three screens, each of those screens could have set up listeners and be setting state, even when they aren't visible.

import { singletonHook } from 'react-singleton-hook'

export const useScreenOrientation = singletonHook(
  {
    isLandscape: false,
    screenOrientation: undefined,
  },
  () => {

    return {
      isLandscape: false,
      screenOrientation
    }
  },
)
Enter fullscreen mode Exit fullscreen mode

Step 2
With a basic hook set up now we can start adding some functionality. To start with, let's set the screenOrientation to be the initial screen orientation picked up by react-native-orientation-locker and we can also add in a useState to keep track of it.

import Orientation, { LANDSCAPE } from 'react-native-orientation-locker'
import { singletonHook } from 'react-singleton-hook'

export const useScreenOrientation = singletonHook(
  {
    isLandscape: false,
    screenOrientation: Orientation.getInitialOrientation(),
  },
  () => {
    const [screenOrientation, setScreenOrientation] = useState(Orientation.getInitialOrientation())

    return {
      isLandscape: screenOrientation.includes(LANDSCAPE),
      screenOrientation
    }
  },
)
Enter fullscreen mode Exit fullscreen mode

Step 3
Ok. Now onto the main part to this problem, we need to be listening for screen orientation changes. I have a small helper function here that I use everywhere. It will come in handy because of the caveat mentioned earlier, and it just tells me whether or not we are on an Android device.

import { Platform } from 'react-native'

export const isAndroid = () => Platform.OS === 'android'
Enter fullscreen mode Exit fullscreen mode

Below I set up a useEffect that only fires once because it has no dependencies and then set up two listeners, one for iOS that uses react-native-orientation-locker and another for Android that uses the dimensions event listener from React Native itself (Don't forget to remove the listeners when the hook is destroyed). Basically, then on a screen orientation change we set the state to the correct OrientationType(PORTRAIT, PORTRAIT-UPSIDEDOWN, LANDSCAPE-LEFT, LANDSCAPE-RIGHT). For Android we just check the height against the width to discern whether it's in portrait or landscape. Of course, if when you read this, that react-native-orientation-locker is working consistently for Android then you won't need any of this specific Android code.

import { useEffect, useState } from 'react'
import { Dimensions, ScaledSize } from 'react-native'
import Orientation, { LANDSCAPE, OrientationType } from 'react-native-orientation-locker'
import { singletonHook } from 'react-singleton-hook'


export const useScreenOrientation = singletonHook(
  {
    isLandscape: false,
    screenOrientation: Orientation.getInitialOrientation(),
  },
  () => {
    const [screenOrientation, setScreenOrientation] = useState(Orientation.getInitialOrientation())

     useEffect(() => {
      const onChange = (result: OrientationType) => {
        setScreenOrientation(result)
      }

      const onChangeAndroid = (result: { screen: ScaledSize }) => {
        return onChange(
          result.screen.height > result.screen.width
            ? OrientationType.PORTRAIT
            : OrientationType['LANDSCAPE-LEFT'],
        )
      }

      if (isAndroid()) {
        Dimensions.addEventListener('change', onChangeAndroid)
      } else {
        Orientation.addOrientationListener(onChange)
      }

      return () => {
        if (isAndroid()) {
          Dimensions.removeEventListener('change', onChangeAndroid)
        } else {
          Orientation.removeOrientationListener(onChange)
        }
      }
    }, [])

    return {
      isLandscape: screenOrientation.includes(LANDSCAPE),
      screenOrientation
    }
  },
)
Enter fullscreen mode Exit fullscreen mode

You Made It To The End! πŸŽ‰

Thanks for reading. This was my first time trying to write something like this. Tell me what you thought could be improved and I will try my best to incorporate those improvements in future ones.

Top comments (1)

Collapse
 
abdullahnaveed profile image
Abdullah Naveed

Great article Robert, 100% agree with the solution!