Let's create a carousel in React Native
Tags: react, reactnative, carousel, swiper
Published: 2020-08-24 10:08:30

Sooner or later you're going to need a carousel in one of your projects. Perhaps you want to display a list of images, maybe an introductory tour of your app, or maybe you want your app to have a couple of swipeable screens. Whatever your use case may be, this article can probably help you.

Let's get started. The base of our carousel will be a simple `FlatList` component. The reason for this is simple - it's based on the `ScrollView` component that will enable us to swipe the slides, plus, it implements `VirtualizedList` which we can use for optimization when there are lots of images or performance heavy UI elements in our slides.

First, let's create some dummy data. We'll use [Lorem Picsum](https://picsum.photos/) to get random images, and we'll create random data for 30 slides for our carousel.

```javascript
const { width: windowWidth, height: windowHeight } = Dimensions.get("window");

const slideList = Array.from({ length: 30 }).map((_, i) => {
  return {
    id: i,
    image: `https://picsum.photos/1440/2842?random=${i}`,
    title: `This is the title! ${i + 1}`,
    subtitle: `This is the subtitle ${i + 1}!`,
  };
});
```

Note that we have to add the query parameter `random=${i}` to get a random image for each slide. Otherwise, React Native would cache the first image and use it in place of every image in our carousel.

Next, we'll create a FlatList and pass our `slideList` to the `data` prop. We'll also pass it the `style` prop with `flex: 1` so it covers the whole screen. Lastly, we have to define the way our slides are going to look. This is done using the `renderItem` prop.
We'll create a `Slide` component and use it in the `renderItem` function.

```javascript
function Slide({ data }) {
  return (
    <View
      style={{
        height: windowHeight,
        width: windowWidth,
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <Image
        source={{ uri: data.image }}
        style={{ width: windowWidth * 0.9, height: windowHeight * 0.9 }}
      ></Image>
      <Text style={{ fontSize: 24 }}>{data.title}</Text>
      <Text style={{ fontSize: 18 }}>{data.subtitle}</Text>
    </View>
  );
}

function Carousel() {
  return (
    <FlatList
      data={slideList}
      style={{ flex: 1 }}
      renderItem={({ item }) => {
        return <Slide data={item} />;
      }}
    />
  );
};
```

![Vertical carousel, no snap points](https://dev-to-uploads.s3.amazonaws.com/i/3tvfv3pjztbys8lx91w0.gif)

If we save now, we'll see our slides, but the scroll behavior is not as we want it. We have to make the ScrollView snap to the beginning of every slide. The easiest way to achieve this is to add the `pagingEnabled={true}` prop to the FlatList.

Another thing - our carousel is currently vertical - it scrolls up and down. Most carousels are horizontal, so let's change the orientation, however, keep in mind that if you need to build a vertical carousel it's possible and only requires a couple of changes.

So let's add the `horizontal={true}` prop to our FlatList to make it scroll left and right, and while we're at it, let's add the `showsHorizontalScrollIndicator={false}` prop to hide the scroll indicator.

```javascript
function Carousel() {
  return (
    <FlatList
      data={slideList}
      style={{ flex: 1 }}
      renderItem={({ item }) => {
        return <Slide data={item} />;
      }}
      pagingEnabled
      horizontal
      showsHorizontalScrollIndicator={false}
    />
  );
}
```

![Horizontal carousel, with snap points](https://dev-to-uploads.s3.amazonaws.com/i/e9is5rz4qcz7xf45ac8z.gif)

This looks great, but there's one important thing we're missing. We're probably going to need the index of the active slide. For example, if we're building a carousel for the application introductory tour, we maybe want to have a "Continue" button that gets enabled only when the user reaches the last slide, or if we're building an image gallery, we might want to display a pagination component to let the user know how much images it contains.

I've spent some time optimizing this next part so it might seem a bit complicated. But don't worry, I'll explain everything.

```javascript
function Carousel() {
  const [index, setIndex] = useState(0);
  const indexRef = useRef(index);
  indexRef.current = index;
  const onScroll = useCallback((event) => {
    const slideSize = event.nativeEvent.layoutMeasurement.width;
    const index = event.nativeEvent.contentOffset.x / slideSize;
    const roundIndex = Math.round(index);

    const distance = Math.abs(roundIndex - index);

    // Prevent one pixel triggering setIndex in the middle
    // of the transition. With this we have to scroll a bit
    // more to trigger the index change.
    const isNoMansLand = 0.4 < distance;

    if (roundIndex !== indexRef.current && !isNoMansLand) {
      setIndex(roundIndex);
    }
  }, []);

  // Use the index
  useEffect(() => {
    console.warn(index);
  }, [index]);

  return (
    <FlatList
      data={slideList}
      style={{ flex: 1 }}
      renderItem={({ item }) => {
        return <Slide data={item} />;
      }}
      pagingEnabled
      horizontal
      showsHorizontalScrollIndicator={false}
      onScroll={onScroll}
    />
  );
}
```

First we define `index` with `useState` - this is going to represent the index of the active slide in the carousel. Then we define `indexRef` - a ref value that is kept in sync with the index variable - when the `index` changes, so does the value of `indexRef.current`.

So why are we doing this? The answer is in the next line. The `onScroll` callback does some calculations with the `layoutMeasurement` and `contentOffset` values in order to calculate the current index according to the distance we scrolled. We want to update our `index` whenever the calculated index changes.

The problem is - if we use the `index` variable inside `onScroll` to check whether the calculated index is different from the current index, then we have to put `index` in the dependency array of `useCallback`. This in turn means that every time the index changes, the `onScroll` function changes too, and as it gets passed as a prop to FlatList, it means the list will re-render.

Note that we used `layoutMeasurement.width` and `contentOffset.x` to calculate the current index since the carousel is horizontal. If it were vertical, we would have to use height and y offset.

Then there's the logic behind the `isNoMansLand` variable. This logic prevents the slider to trigger a bunch of `setIndex` calls when we drag the carousel right in the middle of two slides. Here's what happens when we don't implement this logic - when we're in the middle of two slides, the slightest movement triggers the index change. This can lead to lots of re-renders so it's better to avoid it.

The solution has something to do with this: [Schmitt trigger](https://en.wikipedia.org/wiki/Schmitt_trigger)

![No Schmitt trigger](https://dev-to-uploads.s3.amazonaws.com/i/gytsggov3t03usnij5b2.gif)

Now, what we've built so far is already kinda cool, and it might even be enough for your use case, but there's some hidden performance problems with our implementation that could slow down or even crash your app. This is because it's rendering a whole bunch of slides in advance and it also keeps previous slides in memory. FlatList does this by default to improve perceived performance when we're scrolling through the list fast, but in our case it has negative effects on performance.

I've coded up a simple visualization to show which Slides are mounted and which are not, additionally it highlights our current index. The green dots on the bottom represent the mounted slides, the black ones are unmounted, and the red one is the current active slide.

![Horizontal carousel, not optimized](https://dev-to-uploads.s3.amazonaws.com/i/7lz7hazokf8cponcz7kc.gif)

You can notice that the slides are being mounted 10 slides in advance. Additionally, the first 10 slides never get unmounted. This is all a part of FlatList default optimizations that work great for longer lists, but not for our use case.

So let's implement some optimizations. We'll group the optimization props in an object and pass them to FlatList .

```javascript
  const flatListOptimizationProps = {
    initialNumToRender: 0,
    maxToRenderPerBatch: 1,
    removeClippedSubviews: true,
    scrollEventThrottle: 16,
    windowSize: 2,
    keyExtractor: useCallback(e => e.id, []);
    getItemLayout: useCallback(
      (_, index) => ({
        index,
        length: windowWidth,
        offset: index * windowWidth,
      }),
      []
    ),
  };

  <FlatList
    data={slideList}
    style={{ flex: 1 }}
    renderItem={({ item }) => {
      return <Slide data={item} />;
    }}
    pagingEnabled
    horizontal
    showsHorizontalScrollIndicator={false}
    onScroll={onScroll}
    {...flatListOptimizationProps}
  />
```

Here's the explanation for what all of this means.

`initialNumToRender` - This controls how many slides, starting from the first one, will stay rendered at all times. This is useful in lists where we can scroll to top programmatically - in that case we don't want to wait for the first couple of slides to render so FlatList keeps the rendered at all times. We don't need this functionality so it's safe to put `0` here.

`maxToRenderPerBatch` - This controls how many slides will be rendered per batch. Again this is useful when we have a FlatList with many elements and the user can scroll fast to an are of the FlatList where the data hasn't been loaded yet.

`removeClippedSubviews` - This removes views that are out of the FlatLists viewport. Android has this set to true by default, and I recommend setting on iOS as well. It can remove `Image` components from memory and save some resources.

`scrollEventThrottle` - Controls how many scroll events get triggered while the user drags the carousel. Setting it to 16 means the event will trigger every 16ms. We could probably get away with setting this to a higher number, but 16 seems to work fine.

`windowSize` - This controls how many slides are mounted up front, and how many slides stay mounted behind the current index.
It actually controls the width of the window which VirtualizedList uses to render items - everything inside the window is rendered, and outside of it is blank. If we set this prop to, for example 2, the window will be twice the width of the FlatList. The pink line in the following vizualization is signifies the window.

![Window vizualization](https://dev-to-uploads.s3.amazonaws.com/i/geyc6g4n0zhkddwu0i9m.gif)

For this carousel example, the value 2 works great, but you can experiment with it if you feel like it.

`keyExtractor` - React uses this for internal optimizations. Adding and removing slides might break without this. Also, it removes a warning so that's good.

`getItemLayout` - an optional optimization that allows skipping the measurement of dynamic content if we know the size (height or width) of items ahead of time. In our case the width of the items is always `windowWidth`. Note that if you want your carousel to be vertical, you have to use `windowHeight` instead.

In the end we can move the style outside of the component definition and wrap the `renderItem` function in `useCallback` to avoid our FlatList re-rendering unnecessarily.

One more thing we can do to further optimize our carousel is wrap our Slide element in `React.memo`.

That's it! I've added a pagination component and tweaked the styles a bit and here's how the end product looks like.

![Final carousel](https://dev-to-uploads.s3.amazonaws.com/i/pb3nbu331ov9hxo4c9yu.gif)

You can try it out yourself: https://snack.expo.io/@hrastnik/carousel