How to Handle Infinite Loop in React Carousel

reactjavascriptcsscarousel

by Demaspira Aulia at 13 January 2021

Hi everyone!
This is part 4 of my Simple React Carousel series, it will also be the last part of this series, next I will try to release a package based on this series!

In this last part, I will talk about how to handle infinite loop in Carousel, which mean we can go back to the first item when we click next from the first item.

Prequisites

Check my previous part on this series to see how to create a working Simple React Carousel, or you can also clone the project from previous part from my Github repo.

Handle infinite loop

To tell the Carousel component that we want to show an infinite loop, we will need to pass a new props to it. The props name will be infiniteLoop.

Next we need to clone the last item to be placed before the first item and clone the first item to be placed after the last item. We need to do this so that when the Carousel reach the end, it will still be able to render the first item and after that we move the carousel to the actual first item.

To move the carousel to the actual first or last item, we need to add onTransitionEnd property to our div with carousel-content class. In the onTransitionEnd, we will check disable the animation so that when we move the Carousel to the actual location there is no animation so to the user eyes it doesn't look any different. After disabling the animation, we need to change the currentIndex of the Carousel. After the currentIndex is modified, then we enable the animation again.

index.js

 // ...
 <div
     show={3}
+    infiniteLoop={true}
 >
 // ...
Enter fullscreen mode Exit fullscreen mode

And here is the completed Carousel.js file.

Carousel.js

const Carousel = (props) => {
    const {children, show, infiniteLoop} = props

    const [currentIndex, setCurrentIndex] = useState(infiniteLoop ? show : 0)
    const [length, setLength] = useState(children.length)

    const [isRepeating, setIsRepeating] = useState(infiniteLoop && children.length > show)
    const [transitionEnabled, setTransitionEnabled] = useState(true)

    const [touchPosition, setTouchPosition] = useState(null)

    // Set the length to match current children from props
    useEffect(() => {
        setLength(children.length)
        setIsRepeating(infiniteLoop && children.length > show)
    }, [children, infiniteLoop, show])

    useEffect(() => {
        if (isRepeating) {
            if (currentIndex === show || currentIndex === length) {
                setTransitionEnabled(true)
            }
        }
    }, [currentIndex, isRepeating, show, length])

    const next = () => {
        if (isRepeating || currentIndex < (length - show)) {
            setCurrentIndex(prevState => prevState + 1)
        }
    }

    const prev = () => {
        if (isRepeating || currentIndex > 0) {
            setCurrentIndex(prevState => prevState - 1)
        }
    }

    const handleTouchStart = (e) => {
        const touchDown = e.touches[0].clientX
        setTouchPosition(touchDown)
    }

    const handleTouchMove = (e) => {
        const touchDown = touchPosition

        if(touchDown === null) {
            return
        }

        const currentTouch = e.touches[0].clientX
        const diff = touchDown - currentTouch

        if (diff > 5) {
            next()
        }

        if (diff < -5) {
            prev()
        }

        setTouchPosition(null)
    }

    const handleTransitionEnd = () => {
        if (isRepeating) {
            if (currentIndex === 0) {
                setTransitionEnabled(false)
                setCurrentIndex(length)
            } else if (currentIndex === length + show) {
                setTransitionEnabled(false)
                setCurrentIndex(show)
            }
        }
    }

    const renderExtraPrev = () => {
        let output = []
        for (let index = 0; index < show; index++) {
            output.push(children[length - 1 - index])
        }
        output.reverse()
        return output
    }

    const renderExtraNext = () => {
        let output = []
        for (let index = 0; index < show; index++) {
            output.push(children[index])
        }
        return output
    }

    return (
        <div className="carousel-container">
            <div className="carousel-wrapper">
                {/* You can alwas change the content of the button to other things */}
                {
                    (isRepeating || currentIndex > 0) &&
                    <button onClick={prev} className="left-arrow">
                        &lt;
                    </button>
                }
                <div
                    className="carousel-content-wrapper"
                    onTouchStart={handleTouchStart}
                    onTouchMove={handleTouchMove}
                >
                    <div
                        className={`carousel-content show-${show}`}
                        style={{
                            transform: `translateX(-${currentIndex * (100 / show)}%)`,
                            transition: !transitionEnabled ? 'none' : undefined,
                        }}
                        onTransitionEnd={() => handleTransitionEnd()}
                    >
                        {
                            (length > show && isRepeating) &&
                            renderExtraPrev()
                        }
                        {children}
                        {
                            (length > show && isRepeating) &&
                            renderExtraNext()
                        }
                    </div>
                </div>
                {/* You can alwas change the content of the button to other things */}
                {
                    (isRepeating || currentIndex < (length - show)) &&
                    <button onClick={next} className="right-arrow">
                        &gt;
                    </button>
                }
            </div>
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

1

And that's it! You can check the finished project in my Github.

This is the end of my React Carousel series, I will try to publish a package based on this series in the future.
Follow me to get the latest info!