How to set state with a setInterval() when a button is clicked? - javascript

I'm trying to update the count of activeIndex within the setInterval when the checkbox is ticked. handleNext() and handlePrevious() are working fine when buttons are clicked but when the checkbox is checked the value of activeIndex is not getting updated in handleNext() but it's getting updated on the screen, so there is no condition check for activeIndex and it goes beyond 3.
import { useState } from "react";
import "./styles.css";
export default function App() {
const [slideTimer, setSlideTimer] = useState(null);
const [activeIndex, setActiveIndex] = useState(0);
const slideDuration = 1000;
const handleNext = () => {
if ((activeIndex) >= 3) {
setActiveIndex(0);
} else {
setActiveIndex(prev => prev + 1);
}
};
const handlePrev = () => {
if (activeIndex <= 0) {
setActiveIndex(3);
} else {
setActiveIndex((prev) => prev - 1);
}
};
const toggleSlider = () => {
if (slideTimer) {
clearInterval(slideTimer);
setSlideTimer(null);
return;
}
setSlideTimer(setInterval(handleNext, slideDuration));
};
return (
<div className="App">
<h1>{activeIndex}</h1>
<button onClick={() => handleNext()}>Next</button>
<button onClick={() => handlePrev()}>Previous</button>
<input onClick={() => toggleSlider()} type="checkbox" />
</div>
);
}
I have tried putting the code for toggleSlider inside useEffect() but that too is not working.

Your problem is that when handleNext gets defined (which occurs on every rerender), it only knows about the variables/state in its surrounding scope at the time that it's defined. As a result, when you queue an interval with:
setInterval(handleNext, slideDuration)
You will be executing the handleNext function that only knows about the component's state at that time. When your component eventually rerenders and sets a new value for activeIndex, your interval will still be exeucting the "old" handleNext function defined in the previous render that doesn't know about the newly updated state. One option to resolve this issue is to make the hanldeNext function not rely on state obtained from its outer scope, but instead, use the state setter function to get the current value:
const handleNext = () => {
setActiveIndex(currIndex => (currIndex + 1) % 4);
};
Above I've used % to cycle the index back to 0, but you very well can also use your if-statement, where you return 0 if currIndex >= 3, or return currIndex + 1 in your else.
I would also recommend that you remove the slideTimer. This state value isn't being used to describe your UI (you can see that as you're not using slideTimer within your returned JSX). In this case, you're better off using a ref to store your interval id:
const slideTimerRef = useRef(0); // instead of the `slideTimer` state
...
clearInterval(slideTimerRef.current);
slideTimerRef.current = null;
...
slideTimerRef.current = setInterval(handleNext, slideDuration);

Related

React child not updating a variable of parent through function

So I'm learning react at the moment and I've been struggling with this issue..
I'm trying to do tic-tac-toe so I've got this code:
import './App.css';
import { useState } from "react"
const X = 1;
const O = -1;
const EMPTY = 0;
var Square = ({idx, click}) =>
{
let [val, setVal] = useState(EMPTY);
return (
<button onClick={() => {click(setVal, idx);}}>{val}</button>
)
}
var Logger = ({state}) =>
{
return (
<button onClick={() => {console.log(state);}}>log</button>
)
}
var App = () =>
{
let [turn, setTurn] = useState(X);
let state = new Array(9).fill(EMPTY);
let squares = new Array(9);
let click = (setValFunc, idx) =>
{
setTurn(-turn);
setValFunc(turn);
state[idx] = turn;
}
for (let i = 0 ; i < 9 ; i++)
{
squares[i] = (<Square click={click} idx={i}/>);
}
return (
<>
<Logger state={state} />
<div>
{squares}
</div>
</>
)
}
export default App;
so the squares ARE changing as I click them, but when I click the log button to log the state array to the console, the state array remains all zeros.
what am I missing here?
Your state has to be a React state again. Otherwise, the state you defined inside the App as a local variable only lasts until the next rerender.
Maintain tic-tac-toe state inside a useState hook
let [state, setState] = useState(new Array(9).fill(EMPTY));
Update the click handler accordingly.
let click = (setValFunc, idx) => {
setTurn(-turn);
setValFunc(turn);
setState((prevState) =>
prevState.map((item, index) => (index === idx ? turn : item))
);
};
In React, the state concept is important.
In your case, you need to understand what is your state and how you can model it.
If you are doing a Tic-Tac-Toe you will have:
the board game: a 3 by 3 "table" with empty, cross or circle signs
This can be modeled by an array of 9 elements as you did.
But then you need to store this array using useState otherwise between -re-renders your state array will be recreated every time.
I advise you to read https://reactjs.org/docs/lifting-state-up.html and https://beta.reactjs.org/learn.

Mutating ref value from watcher in Vue 3 with Composition API

I'm trying to mutate a ref within a watch function but it doesn't seem to work. I'm using watch to pay attention if my array length changes, whenever that changes I need to compare the old length with the new one and then mutate a ref (selectedTitle) if the newValue is less than the oldValue.
setup() {
const slots = useSlots();
const tabTitles = computed(() =>
slots.default()[0].children.map((tab) => tab.props.title)
);
const tabTitlesLength = computed(() => tabTitles.value.length);
let selectedTitle = ref(tabTitles.value[0]);
provide("selectedTitle", selectedTitle);
provide("tabTitles", tabTitles);
watch(tabTitlesLength, (currentValue, oldValue) => {
if (currentValue < oldValue) {
selectedTitle = tabTitles.value[0];
} else {
console.log("fuera del if");
}
});
return {
tabTitles,
selectedTitle,
tabTitlesLength,
};
},
With that code the selectedTitle ref never change it always has the last value that received whenever the computed property changes (tabTitles). I just don't understand why since everything else seems to work just fine.
Since selectedTitle is ref try with
selectedTitle.value = tabTitles.value[0];

Delete All Array Elements With Animation

I have a react native todo list app.
The task items are represented by an array using useState which is declared as follows:
const [taskItems, setTaskItems] = useState([]);
I'm trying to add a function that deletes all of the items but instead of just setting it like this setTaskItems([]), which simply deletes all items at once, I was hoping to delete the items one at a time starting from the end to create a more animated look.
while(taskItems.length > 0)
{
alert(taskItems.length);
let itemsCopy = [...taskItems];
itemsCopy.splice(-1);
setTaskItems(itemsCopy);
//setTimeout(function(){},180);
}
For some reason the above code causes infinite recursion. If I remove the while loop then the deleteAll function does indeed remove the last item in the list. Based on my placing the alert at the beginning of the function it seems like the length of the array is never decreased.
Why is that?
And is there a better way to achieve this effect?
It happen because in your code taskItem value doesn't change when you use setTaskItems
setTaskItems is an asynchronous function and the updated value of the state property change after the rerender of the component
you can try something like this
const [taskItems, setTaskItems] = useState([]);
const [deleteAll, setDeleteAll] = useState(false)
useEffect(() => {
if(!deleteAll) return;
if(taskItems.lenght === 0){
setDeleteAll(false)
return;
}
setTimeout(() => {
setTaskItems(t => t.slice(0, -1))
}, 180)
}, [deleteAll, taskItems])
const handleDeleteAll = () => {
seDeleteAll(true)
}
That is because taskItems value is retrieved from closure, meaning you can not expect value to be changed during handler execution. Closure recreates only after element rerenders, meaning you can access new state value only in next function execution - during the function execution value of the state never change.
You just need to assign length value to some temp variable, and use it as start/stop indicator for while loop:
let itemsCount = taskItems.length;
while(itemsCount > 0)
{
// Here use itemsCount to determine how much you would need to slice taskItems array
itemsCount -= 1;
}
UPDATED
An idea of how you should do it is:
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const deleteAll = () => {
let counter = items.length;
while (counter > 0) {
setTimeout(() => {
setItems((p) => [...p.slice(0, counter - 1)]);
}, counter * 1000);
counter -= 1;
}
};
It is important to multiply timeout delay with length, in order to avoid simultaneous execution of all timeouts - you need one by one to be deleted.
Also you should clear timeout on unmount or something like that.
Due to asynchronous nature of React useState hook, you need to harness useEffect to do the job. Here's working example of removing all tasks with a 1000 ms delay between items.
function TasksGrid() {
const [taskItems, setTaskItems] = useState([1, 2, 3, 4, 5, 6]);
const [runDeleteAll, setRunDeleteAll] = useState(false);
useEffect(
() => {
if (runDeleteAll) {
if (taskItems.length > 0)
setTimeout(() => setTaskItems(taskItems.slice(0, taskItems.length - 1)), 1000);
else
setRunDeleteAll(false);
}
},
[runDeleteAll, taskItems]
)
const handleOnClick = () => {
setRunDeleteAll(true);
}
return (
<div>
{taskItems.map((taskItem, idx) => <p key={idx}>task {taskItem}</p>)}
<button onClick={handleOnClick}>delete all</button>
</div>
);
}

How to increment a state variable every second in React

I have a list of images in my code for this component called "url". I also have a state variable called currImageIndex. What I want to happen is every time this component is rendered, I would like to start a timer. Every second this timer runs, I would like to increment my state variable currImageIndex. When currImageIndex + 1 is greater than or equal to url length, I would like to reset currImageIndex to zero. The overall goal is to simulate an automatic slideshow of images by just returning <img src={url[currImageIndex]}>. However, I am having issues with incrementing my state variable. I followed some code I found on here, but I can't figure out how to get the increment working. Here is my code.
const { url } = props;
const [currImageIndex, setCurrImageIndex] = useState(0);
console.log(url)
const increment = () => {
console.log(url.length)
console.log(currImageIndex)
if (currImageIndex + 1 < url.length) {
setCurrImageIndex((oldCount) => oldCount + 1)
}
else {
setCurrImageIndex(0)
}
}
useEffect(() => {
const id = setInterval(increment, 1000);
return () => clearInterval(id);
}, []);
return (
<div className={styles.container}>
<div className={styles.header}>Preview of GIF:</div>
<img src={url[currImageIndex]} alt="GIF Preview" className={styles.image} />
<div>{currImageIndex}</div>
</div>
);
When I run this with a url of size 2, the third to last line ("{currImageIndex}") displays the 1 then 2 then 3 and so on. It does not reset ever. My console.log(url.length) prints 2 and my console.log(currImageIndex) prints 0. What I want to happen is currImageIndex should be 0, 1, 0, 1, 0, 1,.... Can anyone help me understand what I am doing wrong? Thank you!
Issue
The issue here is you've a stale enclosure of the currImageIndex state value from the initial render when the increment callback was setup in the interval timer. You're correctly using a functional state to increment the currImageIndex value, but the currImageIndex value in the increment function body is and always will be that of the initial state value, 0.
Solution
A common solution here would be to use a React ref to cache the currImageIndex state so the current value can be accessed in callbacks.
Example:
const [currImageIndex, setCurrImageIndex] = useState(0);
const currentImageIndexRef = useRef();
useEffect(() => {
// cache the current state value
currentImageIndexRef.current = currentImageIndex;
}, [currImageIndex]);
const increment = () => {
// access the current state value
if (currentImageIndexRef.current + 1 < url.length) {
setCurrImageIndex((oldCount) => oldCount + 1)
}
else {
setCurrImageIndex(0)
}
}
useEffect(() => {
const id = setInterval(increment, 1000);
return () => clearInterval(id);
}, []);
A simpler solution is to just access the url value that is closed over in component scope with the state updater callback. Use a ternary operator to check if adding 1 to the current value is still less than the url length and state + 1, otherwise reset by returning 0. Note however that if the url array is dynamic and the length changes this solution breaks for the same reason, the url value from the initial render is closed over in scope.
const increment = () => {
setCurrImageIndex((count) => count + 1 < url.length ? count + 1 : 0);
}
And IMO the simplest solution is to just increment the currImageIndex state always and take the modulus of the url length to always compute a valid, in-range index value. If the url array changes, it doesn't matter as the modulus function will always compute a valid index.
const { url } = props;
const [index, setIndex] = useState(0);
const increment = () => setIndex(i => i + 1);
useEffect(() => {
const id = setInterval(increment, 1000);
return () => clearInterval(id);
}, []);
const currImageIndex = index % url.length;
return (
<div className={styles.container}>
<div className={styles.header}>Preview of GIF:</div>
<img src={url[currImageIndex]} alt="GIF Preview" className={styles.image} />
<div>{currImageIndex}</div>
</div>
);
The reason is that the callback passed on setInterval only access the currImageIndex variable in the first render, not the new value while render so you will always get currImageIndex = 0 and pass the condition if (currImageIndex + 1 < url.length). I update your code and make it work like your expectation: https://stackblitz.com/edit/react-y6fqwt

Detect scroll direction in React js

I'm trying to detect if the scroll event is up or down but I can't find the solution.
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
const Navbar = ({ className }) => {
const [y, setY] = useState(0);
const handleNavigation = (e) => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
};
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", (e) => handleNavigation(e));
}, []);
return (
<nav className={className}>
<p>
<i className="fas fa-pizza-slice"></i>Food finder
</p>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
</nav>
);
};
export default Navbar;
Basically it's always detected as "down" because y in handleNavigation is always 0. If i check the state in DevTool the y state updates but in the handleNavigation doesn't.
Any suggestions what am I doing wrong?
Thanks for your help
TLDR;
Since this answer has drawn some attention, I've just developed an npm package based on it to allow everyone to use it as an independent package/library in their projects. Therefore if you want something to work with right away, you can add it to your project:
Demo
npm i #smakss/react-scroll-direction
or
yarn add #smakss/react-scroll-direction
Read more here.
The answer and its description
This is because you defined a useEffect() without any dependencies, so your useEffect() will only run once, and it never calls handleNavigation() on y changes. To fix this, you need to add y to your dependency array to tell your useEffect() to run whenever the y value changes. Then it would be best if you had another change to take effect in your code, where you are trying to initialize your y with window.scrollY, so you should do this in your useState() like:
const [y, setY] = useState(window.scrollY);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since it's going to run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
If, for some reason, window was unavailable there or you don't want to do it here, you can do it in two separate useEffect()s.
So your useEffect() should be like this:
useEffect(() => {
setY(window.scrollY);
}, []);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => { // return a cleanup function to unregister our function since its gonna run multiple times
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
UPDATE (Working Solutions)
After implementing this solution on my own. I found out some notes should be applied to this solution. So since the handleNavigation() will change y value directly, we can ignore the y as our dependency and then add handleNavigation() as a dependency to our useEffect(), then due to this change we should optimize handleNavigation(), so we should use useCallback() for it. Then the final result will be something like this:
const [y, setY] = useState(window.scrollY);
const handleNavigation = useCallback(
e => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
}, [y]
);
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation]);
After a comment from #RezaSam, I noticed a teeny tiny mistake in the memoized version. Where I call handleNavigation within another arrow function, I found out (via the browser dev tool, event listeners tab) that each component rerender will register a new event to the window, so it might ruin the whole thing.
Working demo:
Final Optimized Solution
After all, I ended up that memoization, in this case, will help us to register a single event, to recognize scroll direction, but it is not fully optimized in printing the consoles because we are consoling inside the handleNavigation function and there is no other way around to print the desired consoles in the current implementation.
So, I realized there is a better way of storing the last page scroll position each time we want to check a new status. Also, to get rid of a vast amount of consoling scrolling up and scrolling down, we should define a threshold (Use debounce approach) to trigger the scroll event change. So I just searched through the web a bit and ended up with this gist, which was very useful. Then with the inspiration of it, I implement a simpler version.
This is how it looks:
const [scrollDir, setScrollDir] = useState("scrolling down");
useEffect(() => {
const threshold = 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false;
return;
}
setScrollDir(scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
How it works?
I will go from top to bottom and explain each code block.
So I just defined a threshold point with the initial value of 0; then, whenever the scroll goes up or down, it will make a new calculation. You can increase it if you don't want to immediately calculate a new page offset.
Then, instead of scrollY, I decide to use pageYOffset, which is more reliable in cross-browsing.
In the updateScrollDir function, we will check if the threshold is met; then, if it is completed, I will specify the scroll direction based on the current and previous page offset.
The most crucial part of it is the onScroll function. I just used requestAnimationFrame to make sure that we are calculating the new offset after the page got rendered wholly after scrolling. And then, with the ticking flag, we will ensure we are just running our event listener callback once in each requestAnimationFrame.
At last, we defined our listener and our cleanup function.
Then, the scrollDir state will contain the actual scroll direction.
Working demo:
Just wanted to come in with a neat solution, it's quite similar to habbahans but looks a little neater in my opinion.
let oldScrollY = 0;
const [direction, setDirection] = useState('up');
const controlDirection = () => {
if(window.scrollY > oldScrollY) {
setDirection('down');
} else {
setDirection('up');
}
oldScrollY = window.scrollY;
}
useEffect(() => {
window.addEventListener('scroll', controlDirection);
return () => {
window.removeEventListener('scroll', controlDirection);
};
},[]);
Here you can just access the hidden state to do what you wish with in your code.
Try this package - react-use-scroll-direction
import { useScrollDirection } from 'react-use-scroll-direction'
export const MyComponent = () => {
const { isScrollingDown } = useScrollDirection()
return (
<div>
{isScrollingDown ? 'Scrolling down' : 'scrolling up'}
</div>
)
}
Most of the answers seems a bit over-engineered in my opinion.
Here's what I use in my nextjs projects:
function useVerticalScrollDirection() {
const [direction, setDirection] = useState('up');
let prevScrollY = 0;
useEffect(() => {
// Using lodash, we set a throttle to the scroll event
// making it not fire more than once every 500 ms.
window.onscroll = throttle(() => {
// This value keeps the latest scrollY position
const { scrollY } = window;
// Checks if previous scrollY is less than latest scrollY
// If true, we are scrolling downwards, else scrollig upwards
const direction = prevScrollY < scrollY ? 'down' : 'up';
// Updates the previous scroll variable AFTER the direction is set.
// The order of events is key to making this work, as assigning
// the previous scroll before checking the direction will result
// in the direction always being 'up'.
prevScrollY = scrollY;
// Set the state to trigger re-rendering
setDirection(direction);
}, 500);
return () => {
// Remove scroll event on unmount
window.onscroll = null;
};
}, []);
return direction;
}
Then I use it my component like this:
function MyComponent() {
const verticalScrollDirection = useVerticalScrollDirection();
{....}
}
I was looking around and couldn't find a simple solution, so I looked into the event itself and there exists a "deltaY" which makes everything way simpler (no need to keep state of the last scroll value). The "deltaY" value shows the change in "y" that the event had (a positive deltaY means it was a scroll down event, and a negative deltaY means it was a scroll up event).
Here's how it works:
componentDidMount() {
window.addEventListener('scroll', e => this.handleNavigation(e));
}
handleNavigation = (e) => {
if (e.deltaY > 0) {
console.log("scrolling down");
} else if (e.deltaY < 0) {
console.log("scrolling up");
}
};
I found this neat & simple solution just few lines of codes
<div onWheel={ event => {
if (event.nativeEvent.wheelDelta > 0) {
console.log('scroll up');
} else {
console.log('scroll down');
}
}}
>
scroll on me!
</div>
onWheel synthetic event returns an event object having an attribute named nativeEvent containing the original event information. wheelDelta is used to detect the direction even if there is no effective scroll (overflow:hidden).
This is original source -> http://blog.jonathanargentiero.com/detect-scroll-direction-on-react/
Here's my React hook solution, useScrollDirection:
import { useEffect, useState } from 'react'
export type ScrollDirection = '' | 'up' | 'down'
type HistoryItem = { y: number; t: number }
const historyLength = 32 // Ticks to keep in history.
const historyMaxAge = 512 // History data time-to-live (ms).
const thresholdPixels = 64 // Ignore moves smaller than this.
let lastEvent: Event
let frameRequested: Boolean = false
let history: HistoryItem[] = Array(historyLength)
let pivot: HistoryItem = { t: 0, y: 0 }
export function useScrollDirection({
scrollingElement,
}: { scrollingElement?: HTMLElement | null } = {}): ScrollDirection {
const [scrollDirection, setScrollDirection] = useState<ScrollDirection>('')
useEffect(() => {
const element: Element | null =
scrollingElement !== undefined ? scrollingElement : document.scrollingElement
if (!element) return
const tick = () => {
if (!lastEvent) return
frameRequested = false
let y = element.scrollTop
const t = lastEvent.timeStamp
const furthest = scrollDirection === 'down' ? Math.max : Math.min
// Apply bounds to handle rubber banding
const yMax = element.scrollHeight - element.clientHeight
y = Math.max(0, y)
y = Math.min(yMax, y)
// Update history
history.unshift({ t, y })
history.pop()
// Are we continuing in the same direction?
if (y === furthest(pivot.y, y)) {
// Update "high-water mark" for current direction
pivot = { t, y }
return
}
// else we have backed off high-water mark
// Apply max age to find current reference point
const cutoffTime = t - historyMaxAge
if (cutoffTime > pivot.t) {
pivot.y = y
history.filter(Boolean).forEach(({ y, t }) => {
if (t > cutoffTime) pivot.y = furthest(pivot.y, y)
})
}
// Have we exceeded threshold?
if (Math.abs(y - pivot.y) > thresholdPixels) {
pivot = { t, y }
setScrollDirection(scrollDirection === 'down' ? 'up' : 'down')
}
}
const onScroll = (event: Event) => {
lastEvent = event
if (!frameRequested) {
requestAnimationFrame(tick)
frameRequested = true
}
}
element.addEventListener('scroll', onScroll)
return () => element.removeEventListener('scroll', onScroll)
}, [scrollDirection, scrollingElement])
return scrollDirection
}
Usage:
const [scrollingElement, setScrollingElement] = useState<HTMLElement | null>(null)
const ref = useCallback(node => setScrollingElement(node), [setScrollingElement])
const scrollDirection = useScrollDirection({ scrollingElement })
<ScrollingContainer {...{ ref }}>
<Header {...{ scrollDirection }}>
</ScrollingContainer>
Based on https://github.com/pwfisher/scroll-intent and https://github.com/dollarshaveclub/scrolldir. Also ported to React here: https://github.com/AnakinYuen/scroll-direction.
Here is my solution that extends some of the ideas found here. It fires only once every direction change and adds some params to fine-tune the hook call
const useScrollDirection = ({
ref,
threshold,
debounce,
scrollHeightThreshold,
}) => {
threshold = threshold || 10;
debounce = debounce || 10;
scrollHeightThreshold = scrollHeightThreshold || 0;
const [scrollDir, setScrollDir] = useState(null);
const debouncedSetScrollDir = _.debounce(setScrollDir, debounce);
useEffect(() => {
let lastScrollY = ref?.current?.scrollTop;
let lastScrollDir;
let ticking = false;
const hasScrollHeightThreshold =
ref?.current?.scrollHeight - ref?.current?.clientHeight >
scrollHeightThreshold;
const updateScrollDir = () => {
const scrollY = ref?.current?.scrollTop;
if (
Math.abs(scrollY - lastScrollY) < threshold ||
!hasScrollHeightThreshold
) {
ticking = false;
return;
}
const newScroll = scrollY > lastScrollY ? 'down' : 'up';
if (newScroll !== lastScrollDir) {
debouncedSetScrollDir(newScroll);
}
lastScrollY = scrollY > 0 ? scrollY : 0;
lastScrollDir = newScroll;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
ref?.current?.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return scrollDir;
};
Codepen demo
I have been searches this things for an hours. But no one solution work for me, so i write like this and worked for my next.js project.
const [currentScroll, setCurrentScroll] = useState(0)
const [lastScroll, setLastScroll] = useState(0)
const [scrollUp, setScrollUp] = useState(false)
useEffect(()=>{
function handleScroll(){
setCurrentScroll(scrollY)
// check if current scroll
// more than last scroll
if(currentScroll>lastScroll){
setScrollUp('Down')
} else {
setScrollUp('Up')
}
}
// change the last scroll
setLastScroll(scrollY)
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
// this needed to change last scroll
// if currentscroll has change
},[currentScroll]) }
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", (e) => handleNavigation(e));
}, []);
Because you pass an empty array into useEffect, so this hook only runs once and does not render every time y changes.
To change the value of y, we need to re-render when scrolling, so we need to pass a state into this array. In here, we see that window.scrollY always changes once you scroll, so [window.scrollY] can be the best solution to resolve your problem.
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", (e) => handleNavigation(e));
}, [window.scrollY]);
CopePen Demo
Your problem relates to using the dependency in useEffect, you can reference it in this link from the React documentation (note part): useEffect Dependency

Categories

Resources