React useState not updating with setState function - reactjs

I am trying to initialize useState hook with a class object then trying to update it based on a click eventListner but it is not updating more than one time.
function App() {
useEffect(() => {
const func = (e) => {
console.log('click')
handleKeyPress(10, 0)
}
document.addEventListener('click', func)
return () => document.removeEventListener('click', func)
}, [])
console.log('BEGIN')
const [player, setPlayer] = useState({
position: { x: 100, y: 100 },
direction: 'right',
})
console.log(player.position)
const handleKeyPress = (xChange, yChange) => {
const newX = player.position.x + xChange
const newY = player.position.y + yChange
let newDirection = player.direction
if (xChange > 0) {
newDirection = 'right'
}
if (xChange < 0) {
newDirection = 'left'
}
console.log('------->', player.position)
setPlayer((prev) => ({
...prev,
direction: newDirection,
position: { x: newX, y: newY },
}))
}
return (
<GameContainer className="game-container">
<Character positionx={player.position.x} positiony={player.position.y} />
</GameContainer>
)
}
export default App
console log can be seen below ->
as you can see the results it is not updating the x value of player state with every click also note that the string "BEGIN" is printing repeatedly with each click that means I think the render function is reinitiallizing the useState again and again.

useEffect(() => {
const func = (e) => {
console.log('click')
handleKeyPress(10, 0)
}
document.addEventListener('click', func)
return () => document.removeEventListener('click', func)
}, [])
The empty dependency array at the end of this use effect means you only want to run this effect once, when the component first mounts. So you will be using the version of handleKeyPress which existed at that time.
const newX = player.position.x + xChange
const newY = player.position.y + yChange
In handleKeyPress, you're using the player variable to calculate your new values. Since this is the handleKeyPress from the first render, it's also the player from the first render, so position.x and position.y are always 100.
There are two ways you can fix this. First, you can list your dependencies in your useEffect, so that it gets updated every time those dependencies change. In your case, that would be a dependency array of [handleKeyPress], which will actually change on every single render, so at that point you might as well leave out the dependency array entirely. You could also move handleKeyPress inside of the useEffect, and then change the dependency array to [player].
The second option is to make changes to handleKeyPress so it always uses the latest state. You're already using the function version of setPlayer to get the latest state, but you need to use it for all your calculations:
const handleKeyPress = (xChange, yChange) => {
setPlayer((prev) => {
const newX = prev.position.x + xChange;
const newY = prev.position.y + yChange;
let newDirection = prev.direction;
if (xChange > 0) {
newDirection = "right";
}
if (xChange < 0) {
newDirection = "left";
}
return {
...prev,
direction: newDirection,
position: { x: newX, y: newY },
};
});
};

Related

State variable hook does not increment within closure

codesandbox.io/s/github/Tmcerlean/battleship
I am developing a simple board game and need to increment a state variable when a player clicks on a cell with a valid move.
The functionality for validating the move and making the move is all in place, however, I am having difficulty updating the state within the event listener.
I can see that the state is being updated when observed from a useEffect hook, but not when viewed from within the function (even following successive calls).
I have done some reading and believe it could have something to do with having a stale closure, but I am not certain.
My approach to solve this issue was to remove and then re-add the click event listener following every click by the user.
My assumption was that this would cause the correct (newly incremented) state variable to be picked up. Unfortunately, this does not appear to be the case and within the event listener function, the variable is never incremented from 0.
I initialise the state variable here:
const [placedShips, setPlacedShips] = useState(0);
Next, a click event listener is applied to each cell within the gameboard:
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
You will see that the setPlacedships state variable is incremented here and there is a console log to report its value.
I am aware that the useState hook is asynchronous and so console.log will show 0 for the first time it is called. Consequently, I have a useEffect hook deployed outside of the function which also contains a console.log to report the changed value of setPlacedShips:
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips)
}, [placedShips])
After every click the placedShips variable is incremented by 1 and then two functions are run:
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
which is immediately followed by the original setEventListeners function:
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
});
};
As mentioned above, the issue is that the console log within the setEventListeners function constantly remains at 0, while the console log within the useEffect hook increments as expected.
For reference, here is the full component I am working on currently:
import React, { useEffect, useState, useLayoutEffect } from "react";
import gameboardFactory from "../../factories/gameboardFactory";
import Table from "../Reusable/Table";
import "./GameboardSetup.css";
// -----------------------------------------------
//
// Desc: Gameboard setup phase of game
//
// -----------------------------------------------
let playerGameboard = gameboardFactory();
const GameboardSetup = () => {
const [humanSetupGrid, setHumanSetupGrid] = useState([]);
const [ships, _setShips] = useState([
{
name: "carrier",
length: 5,
direction: "horizontal",
},
{
name: "battleship",
length: 4,
direction: "horizontal",
},
{
name: "cruiser",
length: 3,
direction: "horizontal",
},
{
name: "submarine",
length: 3,
direction: "horizontal",
},
{
name: "destroyer",
length: 2,
direction: "horizontal",
},
]);
const [placedShips, setPlacedShips] = useState(0);
const createGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(0);
}
};
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} />;
});
setHumanSetupGrid(result);
};
const setUpPlayerGrid = () => {
// createGrid('grid');
createUiGrid();
};
const currentShip = () => {
return ships[placedShips];
};
const clickListener = (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(e.target.id);
let end = start + currentShip().length - 1;
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips((oldValue) => oldValue + 1);
console.log(placedShips);
}
};
const setEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.addEventListener("click", (e) => {
clickListener(e);
});
cell.addEventListener("mouseover", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.add("test");
});
}
}
});
cell.addEventListener("mouseleave", (e) => {
e.stopImmediatePropagation();
let direction = currentShip().direction;
let start = parseInt(cell.id);
let end = start + currentShip().length - 1;
if (currentShip().direction === "horizontal") {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i++) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
} else {
const newShip = [];
if (playerGameboard.checkValidCoordinates(direction, start, end)) {
for (let i = start; i <= end; i += 10) {
newShip.push(i);
}
newShip.forEach((cell) => {
gameboardArray[cell].classList.remove("test");
});
}
}
});
});
};
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
useEffect(() => {
setUpPlayerGrid();
// setUpComputerGrid();
}, []);
useEffect(() => {
console.log(humanSetupGrid);
}, [humanSetupGrid]);
// Re-render the component to enable event listeners to be added to generated grid
useLayoutEffect(() => {
setEventListeners();
});
useEffect(() => {
removeEventListeners();
setEventListeners();
console.log(placedShips);
}, [placedShips]);
return (
<div className="setup-container">
<div className="setup-information">
<p className="setup-information__p">Add your ships!</p>
<button
className="setup-information__btn"
onClick={() => console.log(placedShips)}
>
Rotate
</button>
</div>
<div className="setup-grid">
<Table grid={humanSetupGrid} />
</div>
</div>
);
};
export default GameboardSetup;
I am quite confused what is happening here and have been stuck on this problem for a couple of days now - if anybody has any suggestions then they would be highly appreciated!
Thank you.
const removeEventListeners = () => {
const gameboardArray = Array.from(document.querySelectorAll(".cell"));
gameboardArray.forEach((cell) => {
cell.removeEventListener("click", (e) => {
clickListener(e);
});
});
};
The above code does not remove any event listeners, which is probably the reason why 0 is still being logged. You pass a new anonymous function to removeEventListener. Since the function is just created it will never remove any event listeners, because it is not registered as an event listener.
Two different functions that do the same are not equal, which is why the event listener is not removed.
const a = (e) => clickListener(e); // passed to addEventListener
const b = (e) => clickListener(e); // passed to removeEventListener
console.log(a == b); //=> false
To add and remove events you cannot use anonymous functions. You either have to use named functions, or store the function in a variable. Then register and remove the event listener using the function name or variable.
Since you only forward the event to the clickListener you can simply replace your event handler registration with:
cell.addEventListener("click", clickListener);
Then remove it using:
cell.removeEventListener("click", clickListener);
Note that this scenario could've been avoided if you passed your event handlers using a more React approach. Instead of using cell.addEventHandler(...) you could've passed the event on creation of this element. eg. <div className='cell' id={counter} onClick={clickListener} />
When working with React you should try to not manipulate the DOM manually. React Components have Synthetic Events, which means that you don't need to add event listeners the vanilla way.
Just add each synthetic event to the cell component with its corresponding handler.
You can do it in the createUiGrid function:
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return <div className="cell" id={counter} onClick={onClickHandler} onMouseOut={onMouseOutHandler} onMouseOver={onMouseOverHandler} />;
});
setHumanSetupGrid(result);
};
And then just move the code you did on vanilla to each corresponding handler (be sure to remove all listener manipulation before testing).

React does not update State (React-Hooks)

I have the following setup:
const templates = [
'dot',
'line',
'circle',
'square',
];
const [currentTemplate, setCurrentTemplate] = useState(0);
const changeTemplate = (forward = true) => {
let index = currentTemplate;
index = forward ? index + 1 : index - 1;
if (index > templates.length - 1) {
index = 0;
} else if (index < 0) {
index = templates.length - 1;
}
setCurrentTemplate(index);
};
useEffect(() => {
console.log(`Current Template is: ${templates[currentTemplate]}`);
}, [currentTemplate]);
useKeypress('ArrowLeft', () => {
changeTemplate(false);
});
useKeypress('ArrowRight', () => {
changeTemplate(true);
});
This is the useKeypress-Hook is used:
import { useEffect } from 'react';
/**
* useKeyPress
* #param {string} key - the name of the key to respond to, compared against event.key
* #param {function} action - the action to perform on key press
*/
export default function useKeypress(key: string, action: () => void) {
useEffect(() => {
function onKeyup(e: KeyboardEvent) {
if (e.key === key) action();
}
window.addEventListener('keyup', onKeyup);
return () => window.removeEventListener('keyup', onKeyup);
}, []);
}
Whenever I press the left or right arrow key, the function gets triggered. But the currentTemplate variable is not changing. It always stays at 0. The useEffect is only triggered when I swich the key from left to right or the other way. Clicking the same key twice, does not trigger useEffect again, but it should! When changing from right to left, the output is Current Template is: square and when changing from left to right, it is Current Template is: line. But this never changes. The value of currentTemplate always stays 0.
What am I missing?
The issue is with your useEffect inside the useKeypress hook.
export default function useKeypress(key: string, action: () => void) {
useEffect(() => {
function onKeyup(e: KeyboardEvent) {
if (e.key === key) action();
}
window.addEventListener('keyup', onKeyup);
return () => window.removeEventListener('keyup', onKeyup);
}, []); // 👈 problem
}
The hook is a single useEffect with no dependencies. Meaning, it will only fire once. This is a problem for the action you are passing into this hook. Whenever the changeTemplate-action changes in your component (i.e. on a rerender), the reference to this is not renewed inside of the useKeypress-hook.
To solve this, you need to add the action to the dependency array of the useEffect:
export default function useKeypress(key: string, action: () => void) {
useEffect(() => {
function onKeyup(e: KeyboardEvent) {
if (e.key === key) action();
}
window.addEventListener('keyup', onKeyup);
return () => window.removeEventListener('keyup', onKeyup);
}, [action]); // 👈 whenever `action` changes, the effect will be updated
}
This is to ensure that the effect is updated whenever the given action is changed. After this change, things should be working as expected.
Optimalizing:
The useEffect will at this point be regenerated for each render, as the changeTemplate function will be instanciated for each render. To avoid this, you can wrap the changeTemplate with a useCallback, so that the function is memoized:
const changeTemplate = useCallback(
(forward = true) => {
let index = currentTemplate;
index = forward ? index + 1 : index - 1;
if (index > templates.length - 1) {
index = 0;
} else if (index < 0) {
index = templates.length - 1;
}
setCurrentTemplate(index);
},
// Regenerate the callback whenever `currentTemplate` or `setCurrentTemplate` changes
[currentTemplate, setCurrentTemplate]
);

Modal trigger - mouse position

I have a question about React. How can I trigger a certain function in the useEffect hook, depending on the position of the cursor?
I need to trigger a function that will open a popup in two cases - one thing is to open a modal after a certain time (which I succeeded with a timeout method), but the other case is to trigger the modal opening function once the mouse is at the very top of the website right in the middle. Thanks for the help.
For now I have that, but I'm struggling with the mouse position part:
function App(e) {
const [modalOpened, setModalOpened] = useState(false);
console.log(e)
useEffect(() => {
const timeout = setTimeout(() => {
setModalOpened(true);
}, 30000);
return () => clearTimeout(timeout);
}, []);
const handleCloseModal = () => setModalOpened(false);
You could use a custom hook that checks the conditions you want.
I'm sure this could be cleaner, but this hook works.
export const useMouseTopCenter = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isTopCenter, setIsTopCenter] = useState(false);
const width = window.innerWidth;
// Tracks mouse position
useEffect(() => {
const setFromEvent = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", setFromEvent);
return () => {
window.removeEventListener("mousemove", setFromEvent);
};
}, []);
// Tracks whether mouse position is in the top 100px and middle third of the screen.
useEffect(() => {
const centerLeft = width / 3;
const centerRight = (width / 3) * 2;
if (
position.x > centerLeft &&
position.x < centerRight &&
position.y < 100
) {
setIsTopCenter(true);
} else {
setIsTopCenter(false);
}
}, [position, width]);
return isTopCenter;
};
Then you would simply add const isCenterTop = useMouseTopCenter(); to your app component.
Checkout this code sandbox, which is just a variation of this custom hook.

useEffect triggers function several times with proper dependencies

i've got Tabs component, it has children Tab components. Upon mount it calculates meta data of Tabs and selected Tab. And then sets styles for tab indicator. For some reason function updateIndicatorState triggers several times in useEffect hook every time active tab changes, and it should trigger only once. Can somebody explain me what I'm doing wrong here? If I remove from deps of 2nd useEffect hook function itself and add a value prop as dep. It triggers correctly only once. But as far as I've read docs of react - I should not cheat useEffect dependency array and there are much better solutions to avoid that.
import React, { useRef, useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { defProperty } from 'helpers';
const Tabs = ({ children, value, orientation, onChange }) => {
console.log(value);
const indicatorRef = useRef(null);
const tabsRef = useRef(null);
const childrenWrapperRef = useRef(null);
const valueToIndex = new Map();
const vertical = orientation === 'vertical';
const start = vertical ? 'top' : 'left';
const size = vertical ? 'height' : 'width';
const [mounted, setMounted] = useState(false);
const [indicatorStyle, setIndicatorStyle] = useState({});
const [transition, setTransition] = useState('none');
const getTabsMeta = useCallback(() => {
console.log('getTabsMeta');
const tabsNode = tabsRef.current;
let tabsMeta;
if (tabsNode) {
const rect = tabsNode.getBoundingClientRect();
tabsMeta = {
clientWidth: tabsNode.clientWidth,
scrollLeft: tabsNode.scrollLeft,
scrollTop: tabsNode.scrollTop,
scrollWidth: tabsNode.scrollWidth,
top: rect.top,
bottom: rect.bottom,
left: rect.left,
right: rect.right,
};
}
let tabMeta;
if (tabsNode && value !== false) {
const wrapperChildren = childrenWrapperRef.current.children;
if (wrapperChildren.length > 0) {
const tab = wrapperChildren[valueToIndex.get(value)];
tabMeta = tab ? tab.getBoundingClientRect() : null;
}
}
return {
tabsMeta,
tabMeta,
};
}, [value, valueToIndex]);
const updateIndicatorState = useCallback(() => {
console.log('updateIndicatorState');
let _newIndicatorStyle;
const { tabsMeta, tabMeta } = getTabsMeta();
let startValue;
if (tabMeta && tabsMeta) {
if (vertical) {
startValue = tabMeta.top - tabsMeta.top + tabsMeta.scrollTop;
} else {
startValue = tabMeta.left - tabsMeta.left;
}
}
const newIndicatorStyle =
((_newIndicatorStyle = {}),
defProperty(_newIndicatorStyle, start, startValue),
defProperty(_newIndicatorStyle, size, tabMeta ? tabMeta[size] : 0),
_newIndicatorStyle);
if (isNaN(indicatorStyle[start]) || isNaN(indicatorStyle[size])) {
setIndicatorStyle(newIndicatorStyle);
} else {
const dStart = Math.abs(indicatorStyle[start] - newIndicatorStyle[start]);
const dSize = Math.abs(indicatorStyle[size] - newIndicatorStyle[size]);
if (dStart >= 1 || dSize >= 1) {
setIndicatorStyle(newIndicatorStyle);
if (transition === 'none') {
setTransition(`${[start]} 0.3s ease-in-out`);
}
}
}
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
useEffect(() => {
const timeout = setTimeout(() => {
setMounted(true);
}, 350);
return () => {
clearTimeout(timeout);
};
}, []);
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
let childIndex = 0;
const childrenItems = React.Children.map(children, child => {
const childValue = child.props.value === undefined ? childIndex : child.props.value;
valueToIndex.set(childValue, childIndex);
const selected = childValue === value;
childIndex += 1;
return React.cloneElement(child, {
selected,
indicator: selected && !mounted,
value: childValue,
onChange,
});
});
const styles = {
[size]: `${indicatorStyle[size]}px`,
[start]: `${indicatorStyle[start]}px`,
transition,
};
console.log(styles);
return (
<>
{value !== 2 ? (
<div className={`tabs tabs--${orientation}`} ref={tabsRef}>
<span className="tab__indicator-wrapper">
<span className="tab__indicator" ref={indicatorRef} style={styles} />
</span>
<div className="tabs__wrapper" ref={childrenWrapperRef}>
{childrenItems}
</div>
</div>
) : null}
</>
);
};
Tabs.defaultProps = {
orientation: 'horizontal',
};
Tabs.propTypes = {
children: PropTypes.node.isRequired,
value: PropTypes.number.isRequired,
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
onChange: PropTypes.func.isRequired,
};
export default Tabs;
useEffect(() => {
if (mounted) {
console.log('1st call mounted');
updateIndicatorState();
}
}, [mounted, updateIndicatorState]);
This effect will trigger whenever the value of mounted or updateIndicatorState changes.
const updateIndicatorState = useCallback(() => {
...
}, [getTabsMeta, indicatorStyle, size, start, transition, vertical]);
The value of updateIndicatorState will change if any of the values in its dep array change, namely getTabsMeta.
const getTabsMeta = useCallback(() => {
...
}, [value, valueToIndex]);
The value of getTabsMeta will change whenever value or valueToIndex changes. From what I'm gathering from your code, value is the value of the selected tab, and valueToIndex is a Map that is re-defined on every single render of this component. So I would expect the value of getTabsMeta to be redefined on every render as well, which will result in the useEffect containing updateIndicatorState to run on every render.

React useEffect with useState and setInterval

using the following code to rotate an array of object through a component DOM. The issue is the state never updates and I can't workout why..?
import React, { useState, useEffect } from 'react'
const PremiumUpgrade = (props) => {
const [benefitsActive, setBenefitsActive] = useState(0)
// Benefits Details
const benefits = [
{
title: 'Did they read your message?',
content: 'Get more Control. Find out which users have read your messages!',
color: '#ECBC0D'
},
{
title: 'See who’s checking you out',
content: 'Find your admirers. See who is viewing your profile and when they are viewing you',
color: '#47AF4A'
}
]
// Rotate Benefit Details
useEffect(() => {
setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [])
the output I get looks like the following image. I can see the useState 'setBenefitsActive' is being called but 'benefitsActive' is never updated.
You pass no dependencies to useEffect meaning it will only ever run once, as a result the parameter for setInterval will only ever receive the initial value of benefitsActive (which in this case is 0).
You can modify the existing state by using a function rather than just setting the value i.e.
setBenefitsActive(v => v + 1);
Some code for your benefit!
In your useEffect as #James suggested, add a dependency to the variable that's being updated. Also don't forget to clean up your interval to avoid memory leaks!
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
//Clean up can be done like this
return () => {
clearInterval(rotationInterval);
}
}, [benefitsActive]) // Add dependencies here
Working Sandbox : https://codesandbox.io/s/react-hooks-interval-demo-p1f2n
EDIT
As pointed out by James this can be better achieved by setTimeout with a much cleaner implementation.
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setTimeout(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [benefitsActive]) // Add dependencies here
Here, a sort of interval is created automatically due to the useEffect being called after each setTimeout, creating a closed loop.
If you still want to use interval though the cleanup is mandatory to avoid memory leaks.
When you pass a function to setInterval, you create a closure, which remembers initial value of benefitsActive. One way to get around this is to use a ref:
const benefitsActive = useRef(0);
// Rotate Benefit Details
useEffect(() => {
const id = setInterval(() => {
console.log(benefits.length);
console.log(benefitsActive.current);
if (benefitsActive.current >= benefits.length) {
console.log("................................. reset");
benefitsActive.current = 0;
} else {
console.log("................................. increment");
benefitsActive.current += 1;
}
}, 3000);
return () => clearInterval(id);
}, []);
Demo: https://codesandbox.io/s/delicate-surf-qghl6
I've had the same problem and found a perfect solution on
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
using an own hook
import { useRef, useEffect } from "react";
export function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
using it like
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);

Resources