Strange scroll behaviour when using useState in React

Viewed 306

I started implementing a carousel in react, furthermore I wanted to give it an infinite scroll effect, that is if you keep clicking right and reaches the end. It starts from the beginning.

The funny thing is, I had to use useState and I am noticing very strange behavior. It never reaches the last div but goes to the first one instantly (No scrolling effect) and scrolls to the second one.

This the main file where the scrolling is happening.

Carousel.tsx

import React, { FC, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useStyles, useStylesClasses } from "./CarouselCSS";
import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt';

interface Props {
    className?: string;
}

type Scroll = "left" | "right";

type Index = {
    sign: "To" | "By";
    value: number;
}

export const Carousel: FC<Props> = ({className, children}) => {
    const {Root, Carousel} = useStyles();
    const classes = useStylesClasses();

    const ref = useRef(null);
    const firstRun = useRef(true);

    const [activeIndex, setActiveIndex] = useState<Index>({
        sign: "To",
        value: 0
    });

    // Triggers before rendering the page
    // useLayoutEffect(()=>{
    //     console.log(ref.current.children.length);
    // }, [ref])

    useEffect(() => {
        if(!firstRun.current)
        if (activeIndex.sign === "To"){
            console.log({"To": activeIndex.value})
             ref.current.scrollTo({
                 left: (activeIndex.value*ref.current.clientWidth),
                 behavior: "smooth",
             })
        }
        else{
            console.log("By")
            ref.current.scrollBy({
                left: ref.current.clientWidth,
                behavior: "smooth",
            })
        }
        else
        firstRun.current = false;        
    }, [activeIndex])

    // controlling scrolling and making it infinite
    const scroll = (value: Scroll) => {
        switch(value){
            case "left": {
                console.log("Left")
                if(activeIndex.value === 0){
                    setActiveIndex({sign: "By", value: ref.current.children.length - 1});
                }
                else{
                    setActiveIndex({sign: "To", value: activeIndex.value - 1});
                }
            }
            break;
            case "right": {
                console.log("Right")
                if(activeIndex.value === ref.current.children.length - 1){
                    setActiveIndex({sign: "To", value: 0});
                }
                else{
                    setActiveIndex({sign: "By", value: activeIndex.value + 1});
                }
            }
            break;
        }
    }

    return (
        <Root>
            <div onClick={() => {
                        scroll("left");
                    }
                }
            >
                <ArrowRightAltIcon style={{transform: 'rotate(180deg)'}} className={classes.arrow}/>
            </div>
            <Carousel className={className} ref={ref}>
                    {children}
            </Carousel>
            <div onClick={() => {
                    scroll("right");
                }}
            >
                <ArrowRightAltIcon className={classes.arrow}/>
            </div>
        </Root>
    )
}

If this doesn't make proper sense, I am also attaching the code sandbox link to the whole thing.

Update As suggested, the component re-renders and hence why the instant appearance of the 1st div. Now, what I am trying to understand here is, why it re-renders in certain cases and does scrolling in some? Since, I am using useState, shouldn't it re-render it every time?

https://codesandbox.io/embed/keen-wright-9n94s?fontsize=14&hidenavigation=1&theme=dark

Any help is appreciated!

1 Answers

That's because when you push the divs for moving, click events trigger re-render the component. Which means it doesn't scroll, it just shows you the new component with the updated activeIndex. This is why its behavior is not smooth. So you need to find other right way to scroll. To avoid moving to the first one when you hit the last one is to add last index check function into the scroll function. If it's the last index, just return nothing but it's not good solution. I suggest you to go find and learn how to move with scroll api in div box. After learning it, you will find out the solution.

Related