1

Trying to implement some custom scroll behaviour for a slider based on the width. It's using useState, to keep track of the current and total pages inside the slider. This is the code I have ended up with trying to acomplise it but it has some unexpected behaviour.

const [isTimeoutLocked, setIsTimeoutLocked] = useState(false)
const handleScroll = useCallback(() => {
  
    const gridContainer = document.querySelector(".grid");
    const totalPages = Math.ceil(
            gridContainer.scrollWidth / gridContainer.clientWidth + -0.1
        );
    setTotalPages(totalPages);
    const scrollPos = gridContainer.clientWidth + gridContainer.scrollLeft + 2;
    if (gridContainer.scrollWidth > scrollPos) {
        gridContainer.scrollBy({
            left: gridContainer.clientWidth + 20.5,
            behavior: "auto",
            block: "center",
            inline: "center",
        });
        setCurrentPage(currentPage + 1);
        setIsTimeoutLocked(false)
        } else {
            gridContainer.scrollTo(0, 0);
            setCurrentPage(1);
            setIsTimeoutLocked(false)
        }
    },[currentPage]);


    useEffect(() => {
    if(!isTimeoutLocked){
        setTimeout(() => {
            setIsTimeoutLocked(true)
            document.querySelector(".grid") && handleScroll();
        }, 5000);
    }
    
}, [currentPage, displayDate, isTimeoutLocked, handleScroll]);

The problem here is that when the currentPage is displayed in the ui it will reset back to one if the else runs inside the handleScroll function which is totally fine but then it will result back to the previous value instead of going back to 2 when it gets to the next page. Also since I have added the isTimeoutLocked as a dependency to the useEffect since it was asking for it the setTimeout will run more often but I only want to have it as a dependency to get the correct value not so the useEffect runs everytime it changes. The purpose of the isTimeoutLocked is so when you change the content inside the container by changing the current day it has not registered a bunch of timeouts.

meowzart
  • 75
  • 1
  • 8

1 Answers1

0

You should add a cleanup function to your useEffect. Since you mutate isTimeoutLocked in your useEffect it can force multiple setTimeouts to run and that is probably the result of wierd behaviour.

When using setTimeout inside useEffect it is always recommended to use it with cleanup. Cleanup will run before the next useEffect is triggered, that gives you a chance to bail out of next setTimeout triggering. Code for it is this:


useEffect(() => {
    if(!isTimeoutLocked){
        const tid = setTimeout(() => {
            setIsTimeoutLocked(true)
            document.querySelector(".grid") && handleScroll();
        }, 5000);

       // this is the cleanup function that is run before next useEffect
       return () => clearTimeout(tid);
    }

You can find more information here: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup .

Another thing to be careful is the async nature of setState and batching. Since you are using setState inside a setTimeout, React will NOT BATCH them, so EVERY setState inside your handleScroll will cause a rerender (if it was not inside a setTimeout/async function React would batch them). Since you have alot of interconected states you should look into useReducer.

Kaca992
  • 2,211
  • 10
  • 14