How to Create a Custom Image Gallery in React Native

In React Native, there are many different ways to go about displaying a collection of images in a gallery view, and a carousel is one of the most popular methods to achieve this. Using an open-source library like react-native-swiper, or something a bit more advanced like react-native-snap-carousel, provides us with the tools we need to create it, but what if you want to create a custom gallery view with additional functionality?

In this tutorial, we're going to create a custom gallery of images using react-native-snap-carousel and FlatList from React Native, and the open-source library is going to display each image in a carousel view. The FlatList is what we will use to display the thumbnail view for each image below the carousel. The construction of the syncing part between the two is to add a functionality such that when an image in the carousel is scrolled either left or right, the thumb in the FlatList is also going to be scrolled along with it. Of course, to achieve this synchronization between the two, we will use React Hooks such that you will be able to implement such a pattern in your own React Native apps.

Prerequisites

To get the most out of this tutorial, you'll want to familiarize yourself with JavaScript/ES6 and meet the following requirements in your local dev environment:

  • Node.js version >= 12.x.x installed.
  • Have access to a package manager like npm, yarn, or npx.
  • Have react-native-cli installed, or use npx.

Setup a React Native Project

To begin this tutorial, we want to set up a new React Native project and install all of the dependencies that are required to implement our customized carousel image gallery. Open up a terminal window and run each command as mentioned in the following order:

npx react-native init rnPreviewImageGallery

cd rnPreviewImageGallery

yarn add react-native-snap-carousel

I prefer to use react-native-snap-carousel because it does not require the configuration of any additional steps in order to be used on native devices. Plus, it offers different layouts to configure the carousel view, right out of the box.

After installing the dependencies, let's bring in the image assets we'd like to use. For this example, I'm using some free, high-res images of Amsterdam pulled from Unsplash. If you'd like to follow along using the same images, you can find them stored here in the GitHub repo for this tutorial.

After setting up the source images we're going to use, open up the App.js file and initiate it with a title of the screen you'd like to display. Import the following statements, then create an IMAGES object by importing each image using Common JS require statements.

Using the useState React hook, create an array of images called images with each image having a unique id in order to help you differentiate between each object in the array.

const [images, setImages] = useState([]);

You'll notice that the useState hook returns two values in an array. The first value is the current value of the state object, and the second value in the array is the function to update the state value of the first. This why the second value starts with a conventional prefix of a set. You can technically name it anything, but following conventions that are commonly used in the React world is a good practice to follow.

Additionally, we should define some constants that will be used throughout this tutorial so that the overall spacing between each thumbnail and the width and height of each thumbnail can be represented in the FlatList.

To set up the carousel view of an image for different screen sizes, we're going to use the Dimensions API from React Native.

Add the following code snippet to App.js and make sure to define state variables at the top of the App function. Hooks are always called at the top level of a functional component in React. When defining a state, they should be the first thing in the function, especially before returning a JSX.

import React, { useState, useRef } from 'react';
import {
  TouchableOpacity,
  View,
  Text,
  Image,
  FlatList,
  Dimensions
} from 'react-native';

const { width } = Dimensions.get('window');
const SPACING = 10;
const THUMB_SIZE = 80;

const IMAGES = {
  image1: require('./assets/images/1.jpeg'),
  image2: require('./assets/images/2.jpeg'),
  image3: require('./assets/images/3.jpeg'),
  image4: require('./assets/images/4.jpeg'),
  image5: require('./assets/images/5.jpeg'),
  image6: require('./assets/images/6.jpeg'),
  image7: require('./assets/images/7.jpeg')
};

const App = () => {
  const [images, setImages] = useState([
    { id: '1', image: IMAGES.image1 },
    { id: '2', image: IMAGES.image2 },
    { id: '3', image: IMAGES.image3 },
    { id: '4', image: IMAGES.image4 },
    { id: '5', image: IMAGES.image5 },
    { id: '6', image: IMAGES.image6 },
    { id: '7', image: IMAGES.image7 }
  ]);

  return (
    <View style={{ flex: 1, backgroundColor: 'black', alignItems: 'center' }}>
      <Text
        style={{
          color: 'white',
          fontSize: 32,
          marginTop: 50,
          marginBottom: 25
        }}
      >
        Custom Gallery
      </Text>
      {/* Carousel View */}
      {/* Thumbnail component using FlatList */}
    </View>
  );
};

export default App;

To initiate the development server for iOS, execute the command npx react-native run-ios from a terminal window. Similarly, the build command for Android is npx react-native run-android.

Here is the app running after this step on an iOS simulator:

The component library, react-native-snap-carousel, has a vast API containing properties and different layout patterns that are plug-n-use, and it also allows you to implement custom interpolations and animations. You can find more information on how to customize it in the official documentation, here.

For our purposes though, let's stick to the default layout pattern to display a carousel. To create a carousel view, import the component from react-native-snap-carousel by adding the following import statement in the App.js file. Let's also import the Pagination component offered separately by this library to display the dot indicator.

// after other import statements
import Carousel, { Pagination } from 'react-native-snap-carousel';

Next, add a View component after the title in the App.js file. It is going to wrap the Carousel component, which takes a set of required props to work:

  • data, the array of images or items to loop.
  • layout to define the way images are rendered and animated. For this example, we will use the default value.
  • sliderWidth to define the width in pixels for the carousel container.
  • itemWidth to define the width in pixels for each item rendered inside the carousel.
  • renderItem takes an image item from the data array and renders it as a list. To render the image, the Image component from React Native is used.

Then, add the following code snippet in App.js to see the carousel in action:

return (
  <View style={{ flex: 1, backgroundColor: 'black', alignItems: 'center' }}>
    {/* Title JSX Remains same */}
    {/* Carousel View */}
    <View style={{ flex: 1 / 2, marginTop: 20 }}>
      <Carousel
        layout='default'
        data={images}
        sliderWidth={width}
        itemWidth={width}
        renderItem={({ item, index }) => (
          <Image
            key={index}
            style={{ width: '100%', height: '100%' }}
            resizeMode='contain'
            source={item.image}
          />
        )}
      />
    </View>
  </View>
);

In the simulator, you should see the following result:

try our app estimate calculator CTA image

Add a dot indicator

The Pagination component from the react-native-snap-carousel is what we'll be using to display a dot indicator for our carousel, and it requires the following props:

  • activeDotIndex to represent the current image shown in the carousel.
  • dotsLength to calculate how many dots to display based on the number of items or images in the carousel.
  • inactiveDotColor to display the dot indicator color when it is inactive.
  • dotColor to display the dot indicator color when it is active.
  • inactiveDotScale is used to set the value to scale the dot indicator when it's inactive.
  • animatedDuration is used to control the length of dot animation in milliseconds. The default value for it is 250. If you'd like to change the default value to something else, this is the prop you'll want to use.

Add the following code snippet after the Carousel component in App.js file:

<View>
  {/* Carousel Component code remains same */}
  <Pagination
    inactiveDotColor='gray'
    dotColor={'orange'}
    activeDotIndex={indexSelected}
    dotsLength={images.length}
    animatedDuration={150}
    inactiveDotScale={1}
  />
</View>

The value of activeDotIndex is calculated based on the current index of the image item. Let's add a state variable called indexSelected in the App component with a value of zero. It is going to update when the index value of the current image changes. The initial value of this state variable is going to be 0. Next, create a handler method called onSelect()—this will update the value of the current index.

Then, add the following code snippet before rendering the JSX in App component:

const App = () => {
  // code remains same
  const [indexSelected, setIndexSelected] = useState(0);

  const onSelect = indexSelected => {
    setIndexSelected(indexSelected);
  };
};

Now, add a prop to the Carousel component called onSnapToItem. It accepts a callback as a value, and this callback is fired every time the index of the image item changes. In other words, every time the user swipes to the next image. The only argument passed to this callback is the current index of the item, which is updated with the help of the onSelect() handler method.

<Carousel
  // rest remains same
  onSnapToItem={index => onSelect(index)}
/>

If we head back to the simulator, you should see that the dot indicator syncs with the Carousel item now.

Let's add another view component below the View that wraps the carousel to display the total number of images and the current image index number.

// Carousel View
<View
  style={{
    marginTop: 20,
    paddingHorizontal: 32,
    alignSelf: 'flex-end'
  }}
>
  <Text
    style={{
      color: 'white',
      fontSize: 22
    }}
  >
    {indexSelected + 1}/{images.length}
  </Text>
</View>

Here is the result after implementing this step:

Awesome! The configuration for the Carousel component is officially complete. Now, let's dive into how to sync it with a custom FlatList component in the next section.

Create a list of thumbnails using FlatList

Let's display a list of thumbnails using FlatList from React Native using the same array of images from the state variable. This list is going to be displayed at the bottom of the device's screen and is a horizontal list. To achieve this, let's use the position: absolute style property with a bottom of value 80.

Each thumbnail is composed of an Image component, and they have the width and height of the THUMB_SIZE variable we declared earlier. To show the selected thumbnail or the current thumbnail using a ternary operator, let's manipulate the style properties borderWidth and borderColor on this Image component.

It is going to be wrapped by a TouchableOpacity component because its onPress prop is going to fire a handler method, we have yet to create, that allows a user to change the selected image with a tap.

To achieve this, add the following code snippet after Carousel's View:

<FlatList
  horizontal={true}
  data={images}
  style={{ position: 'absolute', bottom: 80 }}
  showsHorizontalScrollIndicator={false}
  contentContainerStyle={{
    paddingHorizontal: SPACING
  }}
  keyExtractor={item => item.id}
  renderItem={({ item, index }) => (
    <TouchableOpacity activeOpacity={0.9}>
      <Image
        style={{
          width: THUMB_SIZE,
          height: THUMB_SIZE,
          marginRight: SPACING,
          borderRadius: 16,
          borderWidth: index === indexSelected ? 4 : 0.75,
          borderColor: index === indexSelected ? 'orange' : 'white'
        }}
        source={item.image}
      />
    </TouchableOpacity>
  )}
/>

The list of thumbnails renders as shown below:

In this mockup image, you will see that the first image is selected. You cannot change the currently selected image yet in the FlatList.

The basic element that is going to allow us to sync the image change between both the Carousel view and the thumbnail is a React hook called useRef.

useRef is a function that returns a mutable ref object whose current property can be initialized to keep track of the current index value for each image, and the index value here is the image selected. Initially, it will be the first thumbnail and the first image shown in the carousel.

First, let's create a ref to serve as the reference of the current image from Carousel component and add it to the App.js file:

const App = () => {
  const carouselRef = useRef();
  // ...
};

Since the Carousel component keeps track of the change of the current index of the image component by triggering a callback called snapToItem(), we can use it to sync with the FlatList.

Start by adding a handler method called onTouchThumbnail() after defining the ref. It accepts one argument called touched which is the index value of the current image selected from the TouchableOpacity component or Carousel. If the value of the argument touched and indexSelected is the same, do nothing. Otherwise, when the value of the touched or indexSelected updates, change the current image in the Carousel and the FlatList at the same time.

const onTouchThumbnail = touched => {
  if (touched === indexSelected) return;

  carouselRef?.current?.snapToItem(touched);
};

Add the ref prop on Carousel component:

<Carousel
  ref={carouselRef}
  //...
/>

Next, add an onPress prop on the TouchableOpacity component:

<TouchableOpacity
  onPress={() => onTouchThumbnail(index)}
  activeOpacity={0.9}
>

Here is the output after this step:

The selection sync itself works, but do you notice the problem with the FlatList component? Right now, it doesn't scroll on its own when an image from the Carousel is selected that is not in the current view on the screen, but we can fix this!

Scroll the FlatList using scrollToOffset

To start, create a new ref called flatListRef in App.js and add the ref prop to FlatList component:

const App = () => {
  // ...
  const flatListRef = useRef();

  return (
    // ...
    <FlatList
      ref={flatListRef}
      // rest remains same
    />
  );
};

The scrollToOffset method available on FlatList can be used to scroll the thumbnails to a certain offset, and it accepts two arguments. The first is called offset which accepts a number as a value. The second argument is the animated property which determines whether the scroll should be animated or not.

The value for the offset is going to be indexSelected of the thumbnail multiplied by the size of the thumbnail. Let's also set the value of animated to true.

Since the FlatList has to scroll on every selection, let's add mutate to the ref inside the handler method, onSelect().

const onSelect = indexSelected => {
  setIndexSelected(indexSelected);

  flatListRef?.current?.scrollToOffset({
    offset: indexSelected * THUMB_SIZE,
    animated: true
  });
};

Here is the output after this step:

Conclusion

In this tutorial, we discussed only one scenario for creating a custom image gallery with FlatList, but there are so many different customization options to look into further. The main objective here is to get familiar with the use of react-native-snap-carousel, useRef hook, and the scrollToOffset method in FlatList.

If you're looking to build a scalable cross-platform app, Crowdbotics is here to help! Our expert PMs and developers can convert your website into a native app or vice-versa or build a brand new universal app from scratch. Get in touch with us today for a detailed quote and timeline!

Resources

Originally published:

May 12, 2021