5
(1)

We want to create a slideshow that can have or autoplay option or manual slide changing functionality with arrows. Let’s write our requirements for this component:

The source code

ImageSlideShow.tsx

import { useEffect, useState } from 'react';
import styled, {css} from 'styled-components';

const Title = styled.h2`
    text-align: center
`
const SlideShowWrapper = styled.div`
    position: relative;
    width: 300px;
    padding: 0 60px;
    margin: 0 auto
`
const SlideShowList = styled.ul<{slideFadeSpeed: number}>`
    list-style: none;
    padding: 0;
    margin: 0;
    height: 300px;
    position: relative;
    li {
        opacity: 0;
        position: absolute;
        top: 0;
        transition: opacity ${props => props.slideFadeSpeed}s;
        &.active {
            opacity: 1;
            z-index: 10;
        }
        img {
            width: 300px;
            height: 300px;
            object-fit: cover;
            user-select: none;
        }
    }
`
const arrowsCommonStyle = css`
    position: absolute;
    top: 50%;
    font-size: 62px;
    transform: translateY(-50%);
`
const ArrowLeft = styled.span`
    cursor: pointer;
    &::before {
        content: '\\2190'
    }
    ${arrowsCommonStyle}
    left: 0;
`
const ArrowRight = styled.span`
    cursor: pointer;
    &::before {
        content: '\\2192'
    }
    ${arrowsCommonStyle}
    right: 0
`

type slideShowProps = {
    autoplay?: boolean;
    timeout?: number;
    fadeSpeed?: number;
}

const ImageSlideShow = ({autoplay, timeout = 3, fadeSpeed = 0.6}: slideShowProps) => {
    const [data, setData] = useState([]);
    const [currentIndex, setCurrentIndex] = useState(0);
    const imagesNr = data.length;

    const slideShowInit = () => {
        if(imagesNr) {
            if(currentIndex === imagesNr - 1) {
                setCurrentIndex(0);
            }
            else {
                setCurrentIndex(prev => prev + 1);
            }
        }
    }
    const handleSlideChange = (direction: string) => {
        const isPrevious = direction === "previous";
        const edge =  isPrevious ? 0 : imagesNr - 1
        const otherEdgeIndex = isPrevious ? imagesNr - 1 : 0
        const step = isPrevious ? -1 : 1;
       
        if(currentIndex === edge) {
            setCurrentIndex(otherEdgeIndex);
        }
        else {
            setCurrentIndex(prev => prev + step);
        }
        
    }

    useEffect(() => {
        fetch('http://localhost:4003/slideshow')
            .then(response => response.json())
            .then(json => {
                setData(json);
            });  
    },[]);

    useEffect(() => {
        autoplay && setTimeout(slideShowInit, timeout * 1000);
    }, [data]);
    
    useEffect(() => {
        autoplay && setTimeout(slideShowInit, timeout * 1000);
    }, [currentIndex]);

    return (
        <div>
            <Title>Slideshow</Title>
            <SlideShowWrapper>
                {!autoplay && 
                    <ArrowLeft onClick={() => handleSlideChange('previous')} />
                }
                <SlideShowList slideFadeSpeed={fadeSpeed}>
                    {data.map(({url, id, altText}, i) => (
                        <li key={id} {...(i === currentIndex ? {className: 'active'} : {})}>
                            <img src={url} alt={altText} />
                        </li>
                    ))}
                </SlideShowList>
                {!autoplay && 
                    <ArrowRight onClick={() => handleSlideChange('next')} />
                }
            </SlideShowWrapper>
        </div>
    )
}

export default ImageSlideShow;

Let me explain

The data is coming from a JSON file:

Loading data

When data load is complete we insert the data value into the data React state.

Setting up the props

Our possible props are autoplay, timeout and fadeSpeed and in a type alias we defined the types for the 3 props, all 3 props being optional(we used the ? in type definitions). The timeout and fadeSpeed got also a default value in case the props are missing.

The rendering

As you can notice we have some conditional rendering going on in this picture. If autoplay is not set or is false the ArrowLeft and ArrowRight styled-components elements will render(since it’s manual control not autoplay). The arrows on click event call the handleSlideChange function with the arguments “previous” or “next”.

In this function we check if direction is “previous”(left arrow was clicked) and then we just set up the direction aware slide change functionality. In the edge constant we try to set if the left arrow was clicked(isPrevious) then the edge will always be the first slide, but if it’s the next button than the edge is always the last slide. When we arrived at an edge we need to jump to the other side of this slideshow list, in order to have a circular slideshow(infinite).

We also need to set conditionally which is the other edge: if we were going to the left and arrived at the first slide, then the other edge is the last element, if we were going to the right and reached last slide then the other edge is the first slide.

What action needs to be taken? Well, if we are clicking on the next button then we add to the current index 1 but if we are clicking on the previous button then we add to the current index -1(2 + -1 = 2 – 1 = 1). On each click we are updating the currentIndex state. When that happens, the HTML is rerendered and another li item get’s the active class:

So the code above instructs the browser to add “active” class on the element if the index of the element is equal with the number in the currentIndex state. In case it’s not equal, don’t even bother adding a class attribute on the li tag.

How does the slideshow work style-wise

Well the basic idea is we absolute position all slides on top of each other initially and they are all invisible(opacity:0). After this the first slide get’s the “active” class and we defined on that active class opacity: 1 and a higher z-index, to give the active slide a higher stacking order, to be above all the other. Since we also added a transition for the opacity, the crossfade transition happens elegantly without seeing any kind of instant stack order change/opacity change.

How about the autoplay mode?

When we add autoplay option, the arrows are not rendered.

After the JSON data is fetched and added to the data state, we call the slideShowInit function in a given seconds.(since we already show the first slide, that is why we start with a timeout. You might ask why didn’t I use the setInterval method. Well useEffects and setIntervals are not really friendly with each other and we won’t see the updated currentIndex in the setInterval cycle. Of course there are some tricks to work around this problem but this solution is also a workaround.

In my slideShowInit function I just set the index of the next slide to the currentIndex state, but if we are already on the last slide then the currentIndex will be 0, to start the cycle over again. When currentIndex get’s a new value, I see that in the useEffect hook and then I call the function again with a timeout. It is just like a setInterval.

Here I create a slide change on every 2 seconds:

That’s it for today guys and of course Like, comment and subscribe.

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 1

No votes so far! Be the first to rate this post.