DEV Community

Cover image for Responsive UX Design with React Native Reflect (Part 1)
Santiago Ferreira
Santiago Ferreira

Posted on • Edited on

Responsive UX Design with React Native Reflect (Part 1)

Description

The following tutorial explains step by step how to create a responsive photo album app with React Native and React Native Reflect that works on Web and Native devices.

Our photo album app will display images in a grid with variable number of columns, image aspect ratio, grid separation, etc. all in a responsive manner. We'll also create several responsive, theme-based UI components, including conditional rendering based on screen size.

Create a new Expo app

Create a new Expo app, choosing "blank (TypeScript)" option when asked.

expo init photo-album-tutorial
# ? Choose a template: expo-template-blank-typescript

cd photo-album-tutorial
yarn start
# Run Expo app on web and iOS (or Android). I'll be using iOS for this
# tutorial, but if you prefer, you can follow along using Android instead.
#
# Press 'w' to open web app
# Press 'i' to open iOS app (or 'a' for Android)

Get some galactic photos!

We'll use axios for making HTTP requests and getting images from NASA's public API

yarn add axios

Replace the contents of App.tsx with the following and follow the comments in the code for an explanation of what we're doing.

import React, { useEffect, useState } from "react";
import { ActivityIndicator, FlatList, SafeAreaView, Image } from "react-native";
import Axios from "axios";

// Items used by FlatList, contains list of images.
type Items = { links: [{ href: string }] }[];

// Data returned by HTTP request
type AxiosData = {
  collection: {
    items: Items;
  };
};

const GET_GALAXY_IMAGES =
  "https://images-api.nasa.gov/search?q=spiral%20galaxies&media_type=image";

export default function App() {
  const [isLoading, setLoading] = useState(true);
  const [data, setData] = useState<Items>([]);

  // Get our data
  useEffect(() => {
    Axios.get<AxiosData>(GET_GALAXY_IMAGES)
      .then(({ data }) => {
        setData(data.collection.items);
      })
      .catch((error) => console.error(error))
      .finally(() => setLoading(false));
  }, []);

  // After loading is done "isLoading", we render a FlatList with the data that
  // was set on the success axios callback above "setData(...)"
  //
  // Then we render each of our images inside FlatList's renderImage prop
  return (
    <SafeAreaView>
      {isLoading ? (
        <ActivityIndicator />
      ) : (
        <FlatList
          data={data}
          keyExtractor={(_item, index) => index.toString()}
          renderItem={({ item }) => (
            <Image
              style={{ height: 100, width: 100 }}
              source={{ uri: item.links[0].href }}
            />
          )}
        />
      )}
    </SafeAreaView>
  );
}

Your App should like like this:

Screens 01

Responsive Number of Columns

Let's make our gallery responsive as follows:

  • Display single column list of images on small screens
  • Display three columns image grid on medium size screens
  • Display four columns image grid on large screens

To accomplish this we'll use react-native-reflect library.

yarn add react-native-reflect

React Native Reflect defines a useStyled() hook that let's us easily define responsive props. Add the following to App.tsx:

import { useStyled } from "react-native-reflect";

Inside App() function:

const { attrs } = useStyled({
  attrs: {
    numColumns: [1, 3, 4],
  },
});

Then, use attrs.numColumns responsive prop with FlatList

<FlatList
  data={data}
  numColumns={attrs.numColumns}
  // NOTE: we need to change FlatList's key to be able to change
  // numColumns on the fly. This is a React Native specification.
  key={attrs.numColumns}
  keyExtractor={(_item, index) => index.toString()}
  renderItem={({ item }) => (
    <Image
      style={{ height: 100, width: 100 }}
      source={{ uri: item.links[0].href }}
    />
  )}
/>

Voilà! Resize your browser window and/or test with different native devices to see how the number of columns update with different screen widths.

Screens 02

Fit Images to Screen's Width

So far, we have renderd our images with a fixed 100x100 width and height dimensions. Let's change this such as our gallery takes 100% of the screen's width and the images inside stretch to fit. Keep in mind that for this tutorial, we'll display all images with the same aspect ratio, so all our calculations are based on this assumption.

Moreover, as of the date of writing this tutorial, React Native Image component requires width and height explicit values. Because of this, we will calculate the actual dimensions of the images based on the number of columns and a customly assigned aspect ratio.

Our first step is to get FlatList's width using FlatList's onLayout prop.

Add the following to App() function:

// keep track of FlatList's width as screen resizes
const [flatListWidth, setFlatListWidth] = useState(0);
// Update FlatList's width
const onLayout = (obj: LayoutChangeEvent) => {
  const width = obj.nativeEvent.layout.width;

  // avoid unnecessary updates
  if (width === flatListWidth) return;

  setFlatListWidth(width);
};

Calculate image dimensions using an aspect ratio, the width of the FlatList and the number of columns. Try changing imageAspectRatio with values such as 16/9, 4/3, 3/4 etc.

const imageAspectRatio = 1;
const imageWidth = flatListWidth / attrs.numColumns;
const imageHeight = imageWidth / imageAspectRatio;

Add onLayout prop and update Image's style with calculated dimensions

<FlatList
  ...
  onLayout={onLayout}
  renderItem={({ item }) => (
    <Image
      style={{ width: imageWidth, height: imageHeight }}
      source={{ uri: item.links[0].href }}
    />
  )}
  ...
/>
)}

Your App should look like this:

Screens 03

Responsive Aspect Ratio

What about displaying images with different aspect ratios on different screen sizes? No problem, that's very easy with react-native-reflect! we just need to provide one more value to useStyled().

const { attrs } = useStyled({
  attrs: {
    numColumns: [1, 3, 4],
    // Aspect ration of 4 / 3 on smaller screens and 1 on larger screens
    imageAspectRatio: [4 / 3, 1],
  },
});

// DELETE LINE const imageAspectRatio = 1;
const imageWidth = flatListWidth / attrs.numColumns;
const imageHeight = imageWidth / attrs.imageAspectRatio;

And that's all for Part 1! So far we have created an image gallery that works on web and native devices. Our image gallery has responsive number of columns and image's aspect ratio. Cool!

Screens 04

Next Steps

On Part 2, we will add more options to our image gallery, such as spacing between images. We will also convert our image gallery into it's own component!

Links

Top comments (2)

Collapse
 
navicsteinr profile image
Navicstein Rotciv

This is wonderfully crafted, responsive system all in one place

Collapse
 
sntx profile image
Santiago Ferreira

Thanks!