Build Advanced React Image Slider Tutorial
Build Advanced React Image Slider Tutorial

In one of the previous posts we implemented a custom slider inside React. This is how it looks like.

Initial project

We can just between slides by using arrows and we can use dots on the bottom to jump to the specific slide.

But a lot of people in comments wrote that we miss 2 key features from the slider this is why we will implement them in this post. These features are auto scroll of our slider (when we don't do anything our slides must change automatically) and secondly we must have a sliding animation between slides and not just jumping between slides.

What do we have

So let's look on the project that we build in the previous video.

const App = () => {
  const slides = [
    { url: "http://localhost:3000/image-1.jpg", title: "beach" },
    { url: "http://localhost:3000/image-2.jpg", title: "boat" },
    { url: "http://localhost:3000/image-3.jpg", title: "forest" },
    { url: "http://localhost:3000/image-4.jpg", title: "city" },
    { url: "http://localhost:3000/image-5.jpg", title: "italy" },
  ];
  const containerStyles = {
    width: "500px",
    height: "280px",
    margin: "0 auto",
  };
  return (
    <div>
      <h1>Hello monsterlessons</h1>
      <div style={containerStyles}>
        <ImageSlider slides={slides} />
      </div>
    </div>
  );
};

This is our parent component where inside we defined an array of slides. We also specified size of our slider in a parent container. It allows our slider to stay fluid. Inside our ImageSlider component we just throw our slides and we are good to go.

const slideStyles = {
  width: "100%",
  height: "100%",
  borderRadius: "10px",
  backgroundSize: "cover",
  backgroundPosition: "center",
};

const rightArrowStyles = {
  position: "absolute",
  top: "50%",
  transform: "translate(0, -50%)",
  right: "32px",
  fontSize: "45px",
  color: "#fff",
  zIndex: 1,
  cursor: "pointer",
};

const leftArrowStyles = {
  position: "absolute",
  top: "50%",
  transform: "translate(0, -50%)",
  left: "32px",
  fontSize: "45px",
  color: "#fff",
  zIndex: 1,
  cursor: "pointer",
};

const sliderStyles = {
  position: "relative",
  height: "100%",
};

const dotsContainerStyles = {
  display: "flex",
  justifyContent: "center",
};

const dotStyle = {
  margin: "0 3px",
  cursor: "pointer",
  fontSize: "20px",
};

const ImageSlider = ({ slides }) => {
  const [currentIndex, setCurrentIndex] = useState(0);
  const goToPrevious = () => {
    const isFirstSlide = currentIndex === 0;
    const newIndex = isFirstSlide ? slides.length - 1 : currentIndex - 1;
    setCurrentIndex(newIndex);
  };
  const goToNext = () => {
    const isLastSlide = currentIndex === slides.length - 1;
    const newIndex = isLastSlide ? 0 : currentIndex + 1;
    setCurrentIndex(newIndex);
  };
  const goToSlide = (slideIndex) => {
    setCurrentIndex(slideIndex);
  };
  const slideStylesWidthBackground = {
    ...slideStyles,
    backgroundImage: `url(${slides[currentIndex].url})`,
  };

  return (
    <div style={sliderStyles}>
      <div>
        <div onClick={goToPrevious} style={leftArrowStyles}>
          
        </div>
        <div onClick={goToNext} style={rightArrowStyles}>
          
        </div>
      </div>
      <div style={slideStylesWidthBackground}></div>
      <div style={dotsContainerStyles}>
        {slides.map((slide, slideIndex) => (
          <div
            style={dotStyle}
            key={slideIndex}
            onClick={() => goToSlide(slideIndex)}
          >
            
          </div>
        ))}
      </div>
    </div>
  );
};

Here is our main ImageSlider component. On the top we have styles for different elements. Inside our component we have just a single state of currentIndex which is enough to know what slide to render. We also have several functions goToPrevious, goToNext, goToSlide which help us to jump between slides.

Also it is important that inside markup we just render a single slide and not all our slides.

Auto scrolling

Now let's implement our first feature and it will be auto sliding. If we don't do anything slider must jump between slides. But at the moment when we click previous or next or we use dots auto sliding feature must be disabled

We don't want to see the behaviour that we click on the next slide and in millisecond after auto slide feature jumps again.

If we are talking just about plain Javascript without React this feature is super easy. We simply define setInterval and inside every single time we just change a slide. But it won't work like this inside React because we have React hooks.

If we are talking about setInterval we must store an ID of our interval between rerenders, we must clear it at some point and this is not easy. And here we can't use setInterval efficiently because we want to abort this interval every single time when user uses arrows.

This is why actually the solution here will be setTimeout. Just to remind you the difference between setTimeout and setInterval is that setTimeout just delays the command and setInterval calls it indefinitely after defined amount of time.

What do we need to do? First of all we must somehow store the ID of our setTimeout to be able to clear it later.

Inside React components we are using useRef hook to store mutable values between renders.

const ImageSlider = ({ slides }) => {
  const timerRef = useRef(null);

  useEffect(() => {
    console.log('useEffect')
    timerRef.current = setTimeout(() => {
      goToNext();
    }, 2000);

  });
}

Here we created timerRef where we store our ID. We also added useEffect which will trigger after every render. Inside we make a setTimeout delay for 2 seconds and jump to the next slide.

This means that every 2 seconds our component changes index, rerenders and create a setTimeout to jump again.

Use effect

As you can see in browser our auto sliding feature is working and our useEffect is being called again and again. Which actually means in our case setTimeout works like setInterval.

But it is not all. At some point we want to clear our setTimeout to prevent it from happening. If we clicked arrows or dots we want to reset setTimeout. We could write this logic in every function but it is much better to just reset setTimeout after every render because any slide change triggers render.

useEffect(() => {
  if (timerRef.current) {
    clearTimeout(timerRef.current);
  }
  timerRef.current = setTimeout(() => {
    goToNext();
  }, 2000);

});

Here we clear our setTimeout after every render.

But we must do better. If our component is destroyed we want to clearTimeout also.

useEffect(() => {
  if (timerRef.current) {
    clearTimeout(timerRef.current);
  }
  timerRef.current = setTimeout(() => {
    goToNext();
  }, 2000);

  return () => clearTimeout(timerRef.current);
});

By adding return function in our useEffect we can remove setTimeout after our component is destroyed.

Sliding animation

Another feature that we want to implement is sliding between our slides. And the main problem is that we simply rerender our image and we don't have animation.

We can't implement animation if we didn't render all our slides at once.

Which actually means that we must render all our slides in the row and we must have enormously big container and with css we must change the position of our slider.

The first thing that I want to do to implement it is to define a width of the slide in parent as a prop.

<ImageSlider slides={slides} parentWidth={500} />

We could calculate it on the fly from the DOM but it is much easier to specify it like this.

Now we can remove container with a single image and render a list instead.

  const slidesContainerStyles = {
    display: "flex",
    height: "100%",
    transition: "transform ease-out 0.3s",
  };
  const getSlideStylesWithBackground = (slideIndex) => ({
    ...slideStyles,
    backgroundImage: `url(${slides[slideIndex].url})`,
    width: `${parentWidth}px`,
  });
  const getSlidesContainerStylesWithWidth = () => ({
    ...slidesContainerStyles,
    width: parentWidth * slides.length,
    transform: `translateX(${-(currentIndex * parentWidth)}px)`,
  });

  return (
    ...
    <div>
      <div style={getSlidesContainerStylesWithWidth()}>
        {slides.map((_, slideIndex) => (
          <div
            key={slideIndex}
            style={getSlideStylesWithBackground(slideIndex)}
          ></div>
        ))}
      </div>
    </div>
  )

Here we rendered a list of our slides and for every image we call getSlideStylesWithBackground. It sets not only slide styles but also image and width. Additionally we wrapped all our slides in container with style getSlidesContainerStylesWithWidth. There we set styles and calculate width and transform for all our slides.

Slides list

All our slides are rendered in a row and animation is applied. Now we just need to add a container with overflow to hide slides on the left and on the right.

const slidesContainerOverflowStyles = {
  overflow: "hidden",
  height: "100%",
};

return (
  ...
  <div style={slidesContainerOverflowStyles}>
    <div style={getSlidesContainerStylesWithWidth()}>
      ...
    </div>
  </div>
)

Initial project

As you can see we successfully implemented animation for our slider.

And actually if you are interested to learn how to build real React project from start to the end make sure to check my React hooks course.

📚 Source code of what we've done