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.
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.
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.
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>
)
As you can see we successfully implemented animation for our slider.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done