React Carousel Gallery

Posted: Sat Sep 07 2024
thumbnail of article -  React Carousel Gallery

In the first step, we’ll create a React application. In the past, the go-to solution for this was a tool provided by the React developers called Create React App (CRA). However, there's now a better option available—Vite. Vite offers several advantages, such as faster development speed and smaller build sizes. For more details, feel free to visit the Vite website. Interestingly, Vite was created by Evan You, the developer behind Vue.js, one of React's main competitors.

Creating a React app is very simple. You just need to enter the command above into the terminal, name your project, and choose from several languages supported by Vite. I chose React with JavaScript.

npm create vite@latest

In this small project, I won’t focus too much on styling, as it's not the main goal. However, I’ll add some basic styling for better visibility using Tailwind via a CDN.

I have a JSON list of images, where each object includes a src and an alt attribute. Always remember to provide descriptive alternative text for images. If you prefer, you can use a simple array of image URLs in your app instead—the choice is yours.

{
	"images": [
		{
			"src": "/dog-1.jpg",
			"alt": "happy Border Collie"
		},
		{
			"src": "/dog-2.jpg",
			"alt": "two dogs playing"
		},
		{
			"src": "/dog-3.jpg",
			"alt": "smile of happy dog"
		},
		{
			"src": "/dog-4.jpg",
			"alt": "eyes and nose of brown dog"
		},
		{
			"src": "/dog-5.jpg",
			"alt": "walking dogs"
		}
	
	]
}

I created a useState hook to store the index of the image I want to display. For now, the image index is hardcoded. It's important to understand that useState is a React hook used to manage state in functional components. To toggle the visibility of images, I used a ternary operator that adds or removes the hidden class. If the current state matches the image index, the image is visible; otherwise, the hidden class is applied.

import { images } from './images.json'
import { useState } from 'react'

function App() {
	const [slide, setSlide] = useState(0)

	return (
		<div className='w-1/2 mx-auto mt-32'>
			{images.map((image, index) => (
				<img src={image.src} alt={image.alt} key={index} 
                  className={`${slide === index ? '' : 'hidden'}`} />
			))}
		</div>
			)
}

export default App

Now it's time to add a bullet indicator. In our case, instead of bullets, we'll use numbers to represent each image for better clarity. This is quite simple; we'll follow a similar approach, but in this case, we only need the index, so we can ignore the image argument. To do this, we'll use _ as a placeholder, which is a common practice when only the second argument, the index, is needed. Don't forget to add 1 to the index when displaying the numbers, as array indices start from 0.

import { images } from './images.json'
import { useState } from 'react'

function App() {
	const [slide, setSlide] = useState(0)

	return (
		<div className='w-1/2 mx-auto mt-32'>
			{images.map((image, index) => (
				<img  src={image.src} alt={image.alt} key={index} className={`h-[400px] w-full object-cover 
                ${slide === index ? '' : 'hidden'}`} />
			))}
			<div className='flex justify-center items-center mt-3 gap-4'>
				{images.map((_, index) => (
					<button key={index} onClick={()=>setSlide(index)}>{index + 1}</button>
				))}
			</div>
		</div>
			</div>
			)
}

export default App

Now we come to an interesting point—why did I write the onClick handler like this: onClick={() => setSlide(ndex)}, wrapping the setSlide function in an anonymous function? If we don't use an anonymous function, like in the first example, React throws an error about too many re-renders. This happens because the function is called immediately after the component renders, rather than when the button is clicked, leading to an infinite render loop. By placing the state setter inside an anonymous function, it only gets called when the button is clicked, and React re-renders the component only when necessary.

To add an "active" class to the indicator, we need to apply specific styles when the index matches the current state. If the index is the same as the state, we'll add the classes font-bold and text-purple-500 to highlight the active indicator. In other cases, we won’t add any additional classes. This can be achieved using a classic example of a ternary operator.

<button key={index} onClick={() => setSlide(index)} 
className={index === slide ? 'text-purple-500 font-bold' : ""}>
						{index +1}
					</button>

The final key feature of our carousel is the navigation buttons for showing the next or previous image. Let’s start with the function to show the previous image. We’ll create a prevSlide function that uses the setSlide function from useState. It's crucial to use the previous state in this function to correctly determine which image to go back to. Additionally, when we reach index 0 and click the "previous" button, we need to wrap around to the last image in the list. This can be achieved by setting the index to images.length - 1. For the next image functionality, we can use a similar approach but simply subtract 1 from the current index. The rest of the implementation involves using a classic ternary operator to handle these conditions.

const prevSlide = () => {
			setSlide(prev => (slide === 0 ? images.length - 1 : prev - 1))
		}

Finally, we have the nextSlide function. This function is very similar to prevSlide, but with a focus on what happens when we reach the last index and click the "next" button. In this case, we need to wrap around to the first image, which is index 0. This ensures that after the last image, the carousel will start again from the beginning. The implementation involves a similar approach to the previous function, with the addition of resetting to index 0 when the end of the list is reached.

const nextSlide = () => {
			setSlide(prev => (slide === images.length - 1 ? 0 : prev + 1))
		}

That’s full code of this simply carousel:

import { images } from './images.json'
import { useState } from 'react'

function App() {
	const [slide, setSlide] = useState(2)


		const prevSlide = () => {
			setSlide(prev => (slide === 0 ? images.length - 1 : prev - 1))
		}
		const nextSlide = () => {
			setSlide(prev => (slide === images.length - 1 ? 0 : prev + 1))
		}

	return (
		<div className='w-1/2 mx-auto mt-32'>
			{images.map((image, index) => (
				<img
					src={image.src}
					alt={image.alt}
					key={index}
					className={`h-[400px] w-full object-cover ${slide === index ? '' : 'hidden'}`}
				/>
			))}
			<div className='flex justify-center items-center mt-3 gap-4'>
				<button onClick={prevSlide}>prev</button>
				{images.map((_, index) => (
					<button key={index} onClick={() => setSlide(index)} className={index === slide ? 'text-purple-500 font-bold' : ""}>
						{index +1}
					</button>
				))}
				<button onClick={nextSlide}>next</button>
			</div>
		</div>
	)
}

export default App

Code: https://github.com/marekgacek45/reactMaster/tree/main/imageCarousel

See on YT: https://youtu.be/_2UCeUkXpzM?si=S4tV4sskPf7NgSMl