DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on

React Native Carousel With Pagination, Auto Play and Infinite Loop Fully Customisable

Image description
I will walk you through a React Native CarouselComponent that handles dynamic data and autoplay functionality. This component also incorporates smooth pagination, user interactions, and automatic scrolling.

Key Features:

  • Auto-scroll with intervals and dynamic offset control
  • Pagination that reflects the active slide
  • Manual interruption of auto-scroll by user interaction
  • Dynamic styling for each carousel item

The Component Breakdown

1. Core Libraries

We are leveraging several key libraries to build this carousel:

  • react-native-reanimated: Provides performant animations and scroll handling.
  • react-native: Provides core components like FlatList, StyleSheet, and View.

2. Shared State and Scroll Handling

The component uses shared values (useSharedValue) for tracking the scroll offset (x and offset). useAnimatedScrollHandler is employed to update the x value dynamically as the user scrolls through the carousel.

const x = useSharedValue(0);
const offset = useSharedValue(0);

const onScroll = useAnimatedScrollHandler({
  onScroll: (e) => {
    x.value = e.contentOffset.x;
  },
  onMomentumEnd: (e) => {
    offset.value = e.contentOffset.x;
  },
});
Enter fullscreen mode Exit fullscreen mode

3. Autoplay Functionality

The carousel automatically scrolls through items with an interval. If the user interacts with the carousel by swiping, the autoplay is paused, and it resumes once scrolling ends.

useEffect(() => {
  if (isAutoPlay) {
    interval.current = setInterval(() => {
      if (offset.value >= (dataList.length - 1) * width) {
        offset.value = 0; // Loop back to the start
      } else {
        offset.value += width;
      }
    }, 1000);
  } else {
    clearInterval(interval.current);
  }
  return () => clearInterval(interval.current);
}, [isAutoPlay, offset, width, dataList.length]);
Enter fullscreen mode Exit fullscreen mode

4. Pagination

Pagination is a simple set of dots that updates based on the current item being viewed. The active dot gets a more prominent style.

const Pagination = ({ data, paginationIndex }) => {
  return (
    <View style={styles.paginationContainer}>
      {data.map((_, index) => (
        <View key={index} style={paginationIndex === index ? styles.activeDot : styles.inactiveDot} />
      ))}
    </View>
  );
};
Enter fullscreen mode Exit fullscreen mode

5. Dynamic Carousel Item Rendering

Each carousel item is rendered within a custom AnimatedScalePressView component that allows scaling when pressed. We also give each item a random background color for a visually distinct appearance.

const renderItem = ({ item, index }) => (
  <TouchableOpacity key={index} onPress={() => onPressItem(item)}>
    <View
      style={{
        padding: 2,
        height: 150,  // Item height value
        backgroundColor: getRandomColor(),
        width: width - 30,  // Item width value
        marginHorizontal: 15,  //item margin
        borderRadius: 10,  // Static border radius
      }}
    />

  </TouchableOpacity>
);
Enter fullscreen mode Exit fullscreen mode

Complete Code

import { StyleSheet, View, useWindowDimensions, TouchableOpacity } from 'react-native';
import React, { useEffect, useRef, useState } from 'react';
import Animated, { scrollTo, useAnimatedRef, useAnimatedScrollHandler, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import AnimatedScalePressView from '../../AnimatedScalePressView';

/*
Usage:
< CarouselComponent data={list||[]} autoPlay onPressItem={onPressItem} />
*/

const Dot = ({ index, paginationIndex }) => {
  return <View style={paginationIndex === index ? styles.activeDot : styles.inactiveDot} />;
};

const Pagination = ({ data, paginationIndex }) => {
  return (
    <View style={styles.paginationContainer}>
      {data.map((_, index) => (
        <Dot index={index} key={index} paginationIndex={paginationIndex} />
      ))}
    </View>
  );
};

const CarouselComponent = ({ data: dataList = [], autoPlay, onPressItem }) => {
  const x = useSharedValue(0);
  const { width } = useWindowDimensions();
  const ref = useAnimatedRef();
  const [currentIndex, setCurrentIndex] = useState(0);
  const [paginationIndex, setPaginationIndex] = useState(0);
  const [isAutoPlay, setIsAutoPlay] = useState(autoPlay);
  const offset = useSharedValue(0);
  const interval = useRef();

  const [data, setData] = useState([...dataList]);

  const viewabilityConfig = {
    itemVisiblePercentThreshold: 50,
  };

  const onViewableItemsChanged = ({ viewableItems }) => {
    if (viewableItems[0] && viewableItems[0].index !== null) {
      setCurrentIndex(viewableItems[0].index);
      setPaginationIndex(viewableItems[0].index % dataList.length);
    }
  };

  const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig, onViewableItemsChanged }]);

  const onScroll = useAnimatedScrollHandler({
    onScroll: (e) => {
      x.value = e.contentOffset.x;
    },
    onMomentumEnd: (e) => {
      offset.value = e.contentOffset.x;
    },
  });

  useDerivedValue(() => {
    scrollTo(ref, offset.value, 0, true);
  });

  useEffect(() => {
    if (isAutoPlay) {
      interval.current = setInterval(() => {
        if (offset.value >= (dataList.length - 1) * width) {
          offset.value = 0;
        } else {
          offset.value += width;
        }
      }, 1000);
    } else {
      clearInterval(interval.current);
    }
    return () => clearInterval(interval.current);
  }, [isAutoPlay, offset, width, dataList.length]);

  const getRandomColor = () => {
    const letters = '0123456789ABCDEF';
    let color = '#';
    for (let i = 0; i < 6; i++) {
      color += letters[Math.floor(Math.random() * 16)];
    }
    return color;
  };

  const renderItem = ({ item, index }) => (
    <AnimatedScalePressView key={index} onPress={() => onPressItem(item)}>
      <View
        style={{
          padding: 2,
          height: 150,
          backgroundColor: getRandomColor(),
          width: width - 30,
          marginHorizontal: 15,
          borderRadius: 10,
        }}
      />
    </AnimatedScalePressView>
  );

  return (
    <View style={styles.container}>
      <Animated.FlatList
        ref={ref}
        style={{ flexGrow: 0 }}
        data={data}
        horizontal
        pagingEnabled
        bounces={false}
        onScroll={onScroll}
        scrollEventThrottle={16}
        showsHorizontalScrollIndicator={false}
        onScrollBeginDrag={() => setIsAutoPlay(false)}
        onMomentumScrollEnd={() => setIsAutoPlay(true)}
        viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
        onEndReached={() => {
          if (data.length === dataList.length * 2) return;
          setData((prevData) => [...prevData, ...dataList]);
        }}
        onEndReachedThreshold={0.5}
        keyExtractor={(_, index) => `carousel_item_${index}`}
        renderItem={renderItem}
        removeClippedSubviews
      />
      {dataList.length > 0 && <Pagination paginationIndex={paginationIndex} data={dataList} />}
    </View>
  );
};

export default CarouselComponent;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  paginationContainer: {
    flexDirection: 'row',
    height: 30, // gap between pagination dots and list
    justifyContent: 'center',
    alignItems: 'center',
  },
  activeDot: {
    backgroundColor: '#800020', // Burgundy
    height: 8,
    width: 30,
    marginHorizontal: 2,
    borderRadius: 12,
  },
  inactiveDot: {
    backgroundColor: '#D3D3D3', // Light grey
    height: 8,
    width: 8,
    marginHorizontal: 2,
    borderRadius: 12,
  },
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

This carousel component can easily fit into various React Native projects, providing a smooth scrolling experience with pagination and autoplay.

Top comments (0)