How I Created a Custom Carousel In React using useRef and useState in Typescript
Tags: react, css, javascript
Published: 2021-11-16 16:39:08

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

```tsx
            <Carousel
            heading="demo"
            n={2}
            g="12px"
            >
             {[...Array(10)].map((_val,index) => {
                    return(
                        <FlexBoxTitleCard
                        title={`item ${index+1}`}
                        key={`item ${index+1}`}
                        />
                    )
                })}
            </Carousel>
```
This is How It is in action:
[Carousel Demo](https://imgur.com/Va3xBe8)

__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
```tsx
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>

    )
}
```

carousel.css
```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%;
}

```

Carousel will have 4 props:
```ts
interface CarouselProps {
    heading:string,
    children:JSX.Element[]
    n?:number,
    g?:string,
}
```
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

```tsx
    const [active,setActive] = useState(0)

    const carouselRef = useRef<HTMLDivElement>(null)
```

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``

```tsx
    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)
            }
        }
    }
```

``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
```css
gridAutoColumns:`calc((100% - (${props.n?props.n:3} - 1)*${props.g?props.g:"32px"})/${props.n?props.n:3})`
```
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