useScreenOrientation - React Native Snippet
Tags: reactnative, typescript, javascript
Published: 2022-03-15 22:45:22

#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:

```javascript
import { Dimensions } from 'react-native'

const isLandscape = () => Dimensions.get('window').width > Dimensions.get('window').height
```

**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](https://reactnavigation.org) 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:

- [react-native-orientation-locker](https://github.com/wonday/react-native-orientation-locker)
- [react-singleton-hook](https://github.com/Light-Keeper/react-singleton-hook)

**Caveat** 🚨
[react-native-orientation-locker](https://github.com/wonday/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](https://github.com/Light-Keeper/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](https://reactnavigation.org) 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.

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

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

**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](https://github.com/wonday/react-native-orientation-locker) and we can also add in a _useState_ to keep track of it.

```javascript
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
    }
  },
)
```

**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.

```javascript
import { Platform } from 'react-native'

export const isAndroid = () => Platform.OS === 'android'
```

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](https://github.com/wonday/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](https://github.com/wonday/react-native-orientation-locker) is working consistently for Android then you won't need any of this specific Android code.

```javascript
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
    }
  },
)
```


#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.