Updated July 10, 2019

Building a Dimensions Hook in React Native

If you need to support both portrait and landscape orientations in your device you're most likely going to be reaching for the Dimensions API that ships with React Native.

My code is littered with calls to Dimensions.get('screen') - but what if a user starts by using their app in portrait and then switches to landscape?

In today's lesson we'll build a hook that gives you the user's current screen dimensions and updates when the device orientation changes.

Starter code

App.js

import React from 'react';
import { View, Dimensions, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  containerLandscape: {
    backgroundColor: '#000',
  },
  box: {
    backgroundColor: 'red',
    height: 100,
  },
});

const screen = Dimensions.get('screen');

export default () => {
  const isLandscape = screen.width > screen.height;
  return (
    <View style={[styles.container, isLandscape && styles.containerLandscape]}>
      <View style={[styles.box, { width: screen.width / 2 }]} />
    </View>
  );
};
Initial demo - 01.gif

For this example app I want the width of our red box to always be 50% of the screen width. I also want the background to be black when in landscape. You can see that it does that if we keep the original orientation. Start in portrait we've got the right width box and right color background. If we switch to landscape though it stays what we originally had.

The Hook

We'll be creating a hook called useScreenDimensions. It should return the screen width, height, and whatever other data is provided to us. When the orientation changes it should update that info.

First, we'll use useState to return the initial dimensions:

App.js

// ...

const useScreenDimensions = () => {
  const [screenData, setScreenData] = useState(Dimensions.get('screen'));

  return screenData;
};

export default () => {
  const screenData = useScreenDimensions();
  const isLandscape = screenData.width > screenData.height;

  return (
    <View style={[styles.container, isLandscape && styles.containerLandscape]}>
      <View style={[styles.box, { width: screenData.width / 2 }]} />
    </View>
  );
};

This gives us the exact same behavior as we had before.

Next we'll listen to any dimension changes via Dimensions.addEventListener. This will call the callback function with an object containing window and screen.

App.js

// ...

const useScreenDimensions = () => {
  const [screenData, setScreenData] = useState(Dimensions.get('screen'));

  useEffect(() => {
    const onChange = (result) => {
      setScreenData(result.screen);
    };

    Dimensions.addEventListener('change', onChange);
  });

  return screenData;
};

// ...

This will update the the screenData variable which will in turn update our component.

If you run it now it works! But we've got a major issue.

Right now the listener is going to run forever - we never tell it when it should stop. This is can cause problems.

To clean up a listener with hooks you need to return a function from the useEffect hook.

For us that means returning a function in which we call Dimensions.removeEventListener;

App.js

// ...

const useScreenDimensions = () => {
  const [screenData, setScreenData] = useState(Dimensions.get('screen'));

  useEffect(() => {
    const onChange = (result) => {
      setScreenData(result.screen);
    };

    Dimensions.addEventListener('change', onChange);

    return () => Dimensions.removeEventListener('change', onChange);
  });

  return screenData;
};

// ...

Finally, it would be valuable to have this hook tell us whether or not the device is in portrait mode rather than depending on the consumer to have to do that every time.

App.js

// ...

const useScreenDimensions = () => {
  const [screenData, setScreenData] = useState(Dimensions.get('screen'));

  useEffect(() => {
    const onChange = (result) => {
      setScreenData(result.screen);
    };

    Dimensions.addEventListener('change', onChange);

    return () => Dimensions.removeEventListener('change', onChange);
  });

  return {
    ...screenData,
    isLandscape: screenData.width > screenData.height,
  };
};

export default () => {
  const screenData = useScreenDimensions();

  return (
    <View
      style={[
        styles.container,
        screenData.isLandscape && styles.containerLandscape,
      ]}
    >
      <View style={[styles.box, { width: screenData.width / 2 }]} />
    </View>
  );
};

And there you have it! An easy to reuse hook that will allow you to listen to any dimension changes in your app.

Final demo - 02.gif

Final Code

App.js

import React, { useEffect, useState } from 'react';
import { View, Dimensions, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  containerLandscape: {
    backgroundColor: '#000',
  },
  box: {
    backgroundColor: 'red',
    height: 100,
  },
});

// const screen = Dimensions.get('screen');

const useScreenDimensions = () => {
  const [screenData, setScreenData] = useState(Dimensions.get('screen'));

  useEffect(() => {
    const onChange = (result) => {
      setScreenData(result.screen);
    };

    Dimensions.addEventListener('change', onChange);

    return () => Dimensions.removeEventListener('change', onChange);
  });

  return {
    ...screenData,
    isLandscape: screenData.width > screenData.height,
  };
};

export default () => {
  const screenData = useScreenDimensions();

  console.log(screenData);
  return (
    <View
      style={[
        styles.container,
        screenData.isLandscape && styles.containerLandscape,
      ]}
    >
      <View style={[styles.box, { width: screenData.width / 2 }]} />
    </View>
  );
};
React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our courses and our private Slack community.

Learn More