Building a Dimensions Hook in React Native

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

Last Updated: July 10, 2019

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 by Example Logo

React Native by Example

Become an expert with React Native by building 10 completely unique apps. Nothing beats learning by doing.

Join the email list to be notified of all new lessons and classes!