DEV Community

Anoop Francis V H
Anoop Francis V H

Posted on

How I Created a Custom Carousel In React using useRef and useState in Typescript

Hello world...
This is my first post hoping I can write more. quick intro about me, I have been a developer for 2 years and one year as a Front-End developer, working on react.
I try to create fun react component from scratch rather than relying on a package or some library like material UI or Ant Design. By this post I aim to explain How I created a custom carousel using react hooks some css and typescript.

How It Works:

Before we start I want to show you how it looks in code and how it looks in action

            <Carousel
            heading="demo"
            n={2}
            g="12px"
            >
             {[...Array(10)].map((_val,index) => {
                    return(
                        <FlexBoxTitleCard
                        title={`item ${index+1}`}
                        key={`item ${index+1}`}
                        />
                    )
                })}
            </Carousel>
Enter fullscreen mode Exit fullscreen mode

This is How It is in action:
Carousel Demo

CODE:

this is the code for my carousel component we will break it down step by step:
this is how the carousel.tsx looks like

import { useRef, useState } from 'react'

interface CarouselProps {
    heading:string,
    children:JSX.Element[]
    n?:number,
    g?:string,
}

export const Carousel = (props:CarouselProps) => {

    const [active,setActive] = useState(0)

    const carouselRef = useRef<HTMLDivElement>(null)


    const scrollToNextElement= () => {
        if(carouselRef.current){
            if(active < carouselRef.current.childNodes.length - (props.n?props.n:3)){
                carouselRef.current.scrollLeft = (carouselRef.current.childNodes[active + 1] as HTMLElement).offsetLeft - (carouselRef.current.parentNode as HTMLElement).offsetLeft;
                setActive(active +1)
            }
        }
    }

    const scrollToPreviousElement = () => {
        console.log(active);
        if(carouselRef.current){
            if(active > 0) {
                carouselRef.current.scrollLeft = (carouselRef.current.childNodes[active - 1] as HTMLElement).offsetLeft - (carouselRef.current.parentNode as HTMLElement).offsetLeft ;
                setActive(active - 1)
            }
        }
    }

    return(
        <div>
            <div>
                <p>{props.heading}</p>
                <div>
                    <span className="nav-button cursor_pointer" onClick={scrollToPreviousElement} style={{marginRight:"32px"}}>{"<"}</span>
                    <span className="nav-button cursor_pointer" onClick={scrollToNextElement}>{">"}</span>
                </div>
            </div>
            <div
            className="carousel-slides"
            ref={carouselRef}
            style={{
                gridAutoColumns:`calc((100% - (${props.n?props.n:3} - 1)*${props.g?props.g:"32px"})/${props.n?props.n:3})`,
                gridGap:props.g
            }}
            >
                {props.children}
            </div>
        </div>

    )
}
Enter fullscreen mode Exit fullscreen mode

carousel.css

.carousel-slides {
  display: grid;
  grid-auto-flow: column;
  /* grid-auto-columns: calc((100% - (var(--n) - 1) * var(--g)) / var(--n));
    grid-gap: var(--g); */
  overflow: hidden;
  scroll-behavior: smooth;
  padding: 40px 0;
}

.carousel-slides::-webkit-scrollbar {
  display: none;
}

.nav-button {
  padding: 8px;
  border: 1px solid black;
  border-radius: 100%;
}

Enter fullscreen mode Exit fullscreen mode

Carousel will have 4 props:

interface CarouselProps {
    heading:string,
    children:JSX.Element[]
    n?:number,
    g?:string,
}
Enter fullscreen mode Exit fullscreen mode

let's break it down,

  1. heading:(required) - Heading For the Carousel
  2. children:(required) - Elements inside a carousel, It doesn't matter if it's JSX.Element or JSX.Element[]
  3. n_:(optional) - number of elements to be shown , default 3
  4. g:(optional) - gap between each carousel element , default 32px
    const [active,setActive] = useState(0)

    const carouselRef = useRef<HTMLDivElement>(null)
Enter fullscreen mode Exit fullscreen mode

I have a state active which denotes the left most element in the JSX.Element array
And I have a reference to the carousel element wrapper carouselRef

    const scrollToNextElement= () => {
        if(carouselRef.current){
            if(active < carouselRef.current.childNodes.length - (props.n?props.n:3)){
                carouselRef.current.scrollLeft = (carouselRef.current.childNodes[active + 1] as HTMLElement).offsetLeft - (carouselRef.current.parentNode as HTMLElement).offsetLeft;
                setActive(active +1)
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

scrollToNextElement function lets you scroll to next element in the carousel. Using carouselRef I am able to access the child nodes and I just change scroll left of the wrapper element to the next elements offset left, it's important substract the parent offsetleft to avoid scrolling the extra padding outside the parent element
the wrapper element itself is a grid component, the inline styling of the element is done so that it can take in prop n to get how many elements to show inside the carousel

gridAutoColumns:`calc((100% - (${props.n?props.n:3} - 1)*${props.g?props.g:"32px"})/${props.n?props.n:3})`
Enter fullscreen mode Exit fullscreen mode

this line of could sets the grid in one single row with as many as n number of items available to see, since the overflow is hidden we cannot see the rest of the elements.
scrollToPreviousElement uses the same logic

What Can I do Next:
add another prop
step:(optional) - prop which accepts number and will be the numbers of elements it will skip when we click next or previous

Scrape both scrollToNextElement and scrollToPreviousElement and change to scrollToElement which accepts a number and skips to that element

Discussion (2)

Collapse
anoopfranc profile image
Anoop Francis V H Author

@lukeshiru Thanks for the suggestion,
Thanks for the feedback on nullish coalescing and yes I am trying to improve my naming conventions
The reason Why I used carouselRef is so that if the elements are of different type I can change my prop from JSX.Element[ ] to JSX.Element but still do the functionality.

Once again I really appreciate the feedback and suggestion

Collapse
lukeshiru profile image
LUKESHIRU

You don't actually need carouselRef, you can just use the length of children instead of carouselRef.current.childNodes.length. Also, I recommend you use better property names, n could be displayElements and g could be gap. Finally, instead of using ternaries like this:

props.prop ? props.prop : defaultValue;
Enter fullscreen mode Exit fullscreen mode

You can just do this (nullish coalescing):

props?.prop ?? defaultValue;
Enter fullscreen mode Exit fullscreen mode

Or you can extract that prop in the header of your component and assign the default value there:

const Component = ({ prop = defaultValue }) => { // ...
Enter fullscreen mode Exit fullscreen mode

Cheers!