React Native responsive scaling on the web
Tags: react, reactnative, javascript, webdev
Published: 2021-08-01 15:25:59

---
title: React Native responsive scaling on the web
published: true
date: 2021-08-01 15:23:00 UTC
tags: #react #reactnative #javascript #webdev
canonical_url: https://mmazzarolo.com/blog/2021-07-31-react-native-responsive-scaling-on-the-web/
---

If you’re a React-Native developer you might already be familiar with the concept of “scaling” views in your apps to look good on devices of different pixel densities and sizes.

There are many ways to do it: from using existing libraries like [`react-native-reponsive-screen`](https://www.npmjs.com/package/react-native-responsive-screen) to [rolling your custom solution](https://github.com/Nirsky/react-native-scaling-example).

> Please notice that if you’re trying to follow the iOS and Android design guidelines in your apps, you shouldn’t scale _all_ your views, otherwise users won’t get any benefit from using devices with bigger screens.

Personally, in most of my apps that require views to be scaled I create a `scale` utility that looks somewhat this:

```javascript
import { PixelRatio, Dimensions, Platform } from "react-native";

// Determine if the device pixel density and size are tablet-like.
// For better accuracy, you can also use the react-native-device-info library.
function isTabletLike() {
  const pixelDensity = PixelRatio.get();
  const windowDimensions = Dimensions.get("window");
  const adjustedWidth = windowDimensions.width * pixelDensity;
  const adjustedHeight = windowDimensions.height * pixelDensity;
  if (pixelDensity < 2 && (adjustedWidth >= 1000 || adjustedHeight >= 1000)) {
    return true;
  } else
    return (
      pixelDensity === 2 && (adjustedWidth >= 1920 || adjustedHeight >= 1920)
    );
}

// Given an input number, scale it to suit devices with different sizes and
// pixel densities.
function scale(size) {
  // Fixed base width that has worked well for most of my use cases
  const baseWidth = isTabletLike() ? 520 : 350;
  const windowDimensions = Dimensions.get("window");
  const shorterWindowDimension = windowDimensions.width > windowDimensions.height
      ? windowDimensions.height
      : windowDimensions.width;
  return (shorterWindowDimension / baseWidth) * size;
}
```

Then, where needed, I can scale dimensions by invoking the `scale` function anywhere in the app code:

```javascript
import { StyleSheet, Text, View } from "react-native";
import { scale } from "utils/scale";
import { BeautifulIcon } from "icons";

function ExampleView() {
  return (
    <View style={styles.root}>
      <Text style={styles.text}>Hello world!</Text>
      <BeautifulIcon size={scale(10)} />
    </View>
  );
}

const styles = StyleSheet.create({
  root: {
    width: "100%",
    height: scale(25),
  },
  text: {
    fontSize: scale(18),
  },
});
```

This approach works fine for the most common use cases, but it has one major flaw: although dimensions are available immediately, they may change so any rendering logic or styles that depend on these constants won’t be updated automatically when the device dimensions are updated.

There are only a handful of cases when Android and iOS device dimensions change (e.g. device rotation, foldable devices), but this issue is particularly evident if you’re using React Native on the web because each time you resize your browser windows you’re technically updating the dimensions.

As an example, here’s how [Ordinary Puzzles](https://github.com/mmazzarolo/ordinary-puzzles-app)’ interface (a React Native puzzle game — which requires being responsive to different sizes given that is playable in the browser) doesn’t scale when the window is resized:

<video controls loop muted playsinline style="max-width: 100%; height: auto; width: 720px; aspect-ratio: 1.312926206">
    <source src="https://mmazzarolo.com/97fe970ef95ca2a6e109031972432eaf/without-scaling.mp4" type="video/mp4"></source>
  </video>

The solution?  
_We must invoke `scale` on every render_ to avoid using a cached result (for example, when we invoke it within `StyleSheet.create`).

Luckily, React Native offers a handy [`useWindowDimensions`](https://reactnative.dev/docs/usewindowdimensions) hook that we can use to create a `useScale` hook to solve our issue:

```javascript
import { useWindowDimensions, Platform } from "react-native";

// Determine if the device pixel density and size are tablet-like.
// For better accuracy, you can also use the react-native-device-info library.
function isTabletLike(windowDimensions) {
  const pixelDensity = windowDimensions.scale;
  const adjustedWidth = windowDimensions.width * pixelDensity;
  const adjustedHeight = windowDimensions.height * pixelDensity;
  if (pixelDensity < 2 && (adjustedWidth >= 1000 || adjustedHeight >= 1000)) {
    return true;
  } else
    return (
      pixelDensity === 2 && (adjustedWidth >= 1920 || adjustedHeight >= 1920)
    );
}

// Returns a scaling function that, given an input number, scale it to suit
// devices with different sizes and pixel densities.
function useScale() {
  const windowDimensions = useWindowDimensions();
  let baseWidth;
  if (Platform.OS === "android" || Platform.OS === "ios") {
    // Fixed base width that has worked well for most of my use cases
    baseWidth = isTabletLike() ? 520 : 350;
  } else {
    // For web, macOS, or Windows builds.
    // Potentially, you can use breakpoints here for a truly responsive design.
    // Or even debounce the result to avoid stressing the CPU while the user is
    // resizing the window.
    baseWidth = 800;
  }
  const shorterWindowDimension = windowDimensions.width > windowDimensions.height
    ? windowDimensions.height
    : windowDimensions.width;
  return (size) => (shorterWindowDimension / baseWidth) * size;
}
```

And then update our code to make sure we generate a new `scale` function on each render:

```javascript
import { StyleSheet, Text, View } from "react-native";
import { useScale } from "utils/scale";
import { BeautifulIcon } from "icons";

function ExampleView() {
  // Make sure our scaling function is always using the most up-to-date
  // dimensions by creating it through a hook.
  const scale = useScale();
  // To use the scaling function in non-inline styles, we can re-create them
  // on each render instead of caching them with "StyleSheet.create" (which
  // doesn't seem to offer too many benefits anyway, from my understanding).
  // Alternatively, you can continue using "StyleSheet.create" for generating
  // styles that don't need to be scaled, and inline just the scaled sizes.
  const styles = createStyles(scale);
  return (
    <View style={styles.root}>
      <Text style={styles.text}>Hello world!</Text>
      <BeautifulIcon size={scale(10)} />
    </View>
  );
}

const createStyles = (scale) => ({
  root: {
    width: "100%",
    height: scale(25),
  },
  text: {
    fontSize: scale(18),
  },
});
```

Here’s the result (ignore the janky jumping around of the interface, it was recorded in development mode):

<video controls loop muted playsinline style="max-width: 100%; height: auto; width: 720px; aspect-ratio: 1.312926206">
    <source src="https://mmazzarolo.com/0e9b76631f6b0051c59a902c0519866d/with-scaling.mp4"></source>
  </video>

The scaling API and its usage are a bit more complicated to use now, but I think the result is worth the effort.  
Also, regardless of scaling or not part of the UI, I think creating styles on each render (or use solutions like `styled-components`) is a good idea to simplify your app theming (e.g., to immediately update the app color based on the device dark/light theme preference, etc.).

> Please notice that the React Native Dimensions API exposes the [`fontscale`](https://reactnative.dev/docs/usewindowdimensions#fontscale) property as well. Some operating systems allow users to scale their font sizes larger or smaller for reading comfort, so you should consider this size when creating/scaling custom text components.