Update state in useEffect shows warning - reactjs

I want to update a state after some other state is updated:
export default function App() {
const [diceNumber, setDiceNumber] = useState(0);
const [rolledValues, setRolledValues] = useState([
{ id: 1, total: 0 },
{ id: 2, total: 0 },
{ id: 3, total: 0 },
{ id: 4, total: 0 },
{ id: 5, total: 0 },
{ id: 6, total: 0 }
]);
const rollDice = async () => {
await startRolingSequence();
};
const startRolingSequence = () => {
return new Promise(resolve => {
for (let i = 0; i < 2500; i++) {
setTimeout(() => {
const num = Math.ceil(Math.random() * 6);
setDiceNumber(num);
}, (i *= 1.1));
}
setTimeout(resolve, 2600);
});
};
useEffect(()=>{
if(!diceNumber) return;
const valueIdx = rolledValues.findIndex(val => val.id === diceNumber);
const newValue = rolledValues[valueIdx];
const {total} = newValue;
newValue.total = total + 1;
setRolledValues([
...rolledValues.slice(0,valueIdx),
newValue,
...rolledValues.slice(valueIdx+1)
])
}, [diceNumber]);
return (
<div className="App">
<button onClick={rollDice}>Roll the dice</button>
<div> Dice Number: {diceNumber ? diceNumber : ''}</div>
</div>
);
}
Here's a sandbox
When the user rolls the dice, a couple setTimeouts will change the state value and resolve eventually. Once it resolves I want to keep track of the score in an array of objects.
So when I write it like this, it works but eslint gives me a warning of a missing dependency. But when I put the dependency in, useEffect will end in a forever loop.
How do I achieve a state update after a state update without causing a forever loop?

Here's a way to set it up that keeps the use effect and doesn't have a dependency issue in the effect: https://codesandbox.io/s/priceless-keldysh-wf8cp?file=/src/App.js
This change in the logic of the setRolledValues call includes getting rid of an accidental mutation to the rolledValues array which could potentially cause issues if you were using it in other places as all React state should be worked with immutably in order to prevent issues.
The setRolledValues has been changed to use the state callback option to prevent a dependency requirement.
useEffect(() => {
if (!diceNumber) return;
setRolledValues(rolledValues => {
const valueIdx = rolledValues.findIndex(val => val.id === diceNumber);
const value = rolledValues[valueIdx];
const { total } = value;
return [
...rolledValues.slice(0, valueIdx),
{ ...value, total: total + 1 },
...rolledValues.slice(valueIdx + 1)
];
});
}, [diceNumber]);
I wouldn't recommend working with it like this, though as it has an issue where if the same number is rolled multiple times in a row, the effect only triggers the first time.
You can move the logic into the rollDice callback instead, which will get rid of both issues that was occurring. https://codesandbox.io/s/stoic-visvesvaraya-402o1?file=/src/App.js
I added a useCallback around rollDice to ensure it doesn't change references so it can be used within useEffects.
const rollDice = useCallback(() => {
const num = Math.ceil(Math.random() * 6);
setDiceNumber(num);
setRolledValues(rolledValues => {
const valueIdx = rolledValues.findIndex(val => val.id === num);
const value = rolledValues[valueIdx];
const { total } = value;
return [
...rolledValues.slice(0, valueIdx),
// newValue,
{ ...value, total: total + 1 },
...rolledValues.slice(valueIdx + 1)
];
});
}, []);

You should be able to just stick all the logic within setRolledValues
useEffect(() => {
if (!diceNumber) return;
setRolledValues((prev) => {
const valueIdx = prev.findIndex(val => val.id === diceNumber);
const newValue = prev[valueIdx];
const { total } = newValue;
newValue.total = total + 1;
return [
...prev.slice(0, valueIdx),
newValue,
...prev.slice(valueIdx + 1)
]
})
}, [diceNumber]);
EDIT: As others have pointed out, useEffect for this application appears to be ill-suited, as you could simply update setRolledValues in your function instead.
If there is some sort of underlying system we're not being shown where you must use an observer pattern like this, you can change the datatype of diceNumber to an object instead, that way subsequent calls to the same number would trigger useEffect

Related

background change every second

I need to make background change every second. if i use setinterval. the background changes too fast.
here's my code:
const { url, id, isButtonPrev, isButtonNext } = useOwnSelector(state => state.sliderReducer);
const img = useRef<HTMLImageElement>(null);
const dispatch = useOwnDispatch();
Here's function which chang background
const setBackGround = (index: number | null = null) => {
console.log(index)
if(img.current) {
img.current.src = `${url}${id < 10 ? `0${id}` : `${id}`}.jpg`;
img.current.onload = () => {
document.body.style.backgroundImage = `url(${img.current?.src})`;
if (index) dispatch(setId(index));
dispatch(isButton(''));
}
}
}
then I call this function:
setBackGround();
setInterval(() => {
setBackGround(id + 1);
}, 1000);
but background change very fast
I also tried to use the useEffect hook. But it didn’t help either
useEffect( () => {
const intervalID = setInterval(() => {
setBackGround(id + 1);
}, 1000);
return clearInterval(intervalID);
}, []);
useRef returns an object like {current: "value"}.Therefore, you need to use it as follows.
const imgRef = useRef<HTMLImageElement>(null);
if(imgRef.current){
imgRef.current.src = url;
}

Why does useEffect return the first value and then last value when called instead of each render?

I have a group of functions that are responsible for adding and removing sections from my form in my app. I use a counter (subjectAddressCounter) in my state that keeps track of the iteration of the section being added. This works as expected when the user clicks the buttons to add and remove the sections, however when I call the addSection() function on init in a form mapping function generateAdditiveFormKeys() the counter jumps from 0 to 2 and so it only calls the callback function in the useEffect responsible for adding the section one time, which means my form doesn't build correctly.
So to be clear. I expect that for each time the function is called the counter will iterate by one, but what's happening on init is that my function is called twice, but in the useEffect the counter goes from 0 to 2 instead of 0, 1, 2. This causes my callback function to only be called once and then the sections are out of sync.
What am I doing wrong?
Please see the relevant code below. Also, is there a way to simplify this pattern so I don't need to use three functions just to add or remove a section?
const FormGroup = prop => {
const [subjectAddressCounter, setSubjectAddressCounter] = useState(0);
const addValues = () => {
const newSection = subjectAddressCopy.map(copy => {
const clone = {
...copy,
prop: copy.prop = `${copy.prop}_copy_${subjectAddressCounter}`,
multiTypes: copy.multiTypes ? copy.multiTypes.map(multi => {
const cloneMultiTypes = {
...multi,
prop: multi.prop = `${multi.prop}_copy_${subjectAddressCounter}`,
};
return cloneMultiTypes;
}) : null,
};
return clone;
});
...
};
const removeValues = () => {
...
}
const prevSubjectAddressCounterRef = useRef<number>(subjectAddressCounter);
useEffect(() => {
prevSubjectAddressCounterRef.current = subjectAddressCounter;
if (prevSubjectAddressCounterRef.current < subjectAddressCounter) {
// call function to addValues
// this is only being called once on init even though subjectAddressCounter starts at 0 and goes to 2
addValues();
}
if (prevSubjectAddressCounterRef.current > subjectAddressCounter) {
// call function to removeValues
removeValues();
}
}, [subjectAddressCounter]);
const addSection = section => {
if (section.section === SectionTitle.subjectAddress) {
// this gets called twice on init
setSubjectAddressCounter(prevCount => prevCount + 1);
}
};
const removeSection = section => {
if (section.section === SectionTitle.subjectAddress) {
setSubjectAddressCounter(prevCount => prevCount - 1);
}
};
const generateAdditiveFormKeys = reportResponse => {
const {
entity_addresses_encrypted: entityAddressesEncrypted, // length equals 3
} = reportResponse;
let additiveAddresses = {};
if (entityAddressesEncrypted?.length > 1) {
entityAddressesEncrypted.forEach((entityAddress, i) => {
if (i === 0) return;
if (subjectAddressCounter < entityAddressesEncrypted.length - 1) {
addSection({ section: SectionTitle.subjectAddress });
}
const keysToAdd = {
[`question_11_address_copy_${i - 1}`]: entityAddress.address,
[`question_12_city_copy_${i - 1}`]: entityAddress.city,
[`question_13_state_copy_${i - 1}`]: entityAddress.state,
[`question_14_zip_copy_${i - 1}`]: entityAddress.zip,
[`question_15_country_copy_${i - 1}`]: entityAddress.country,
};
additiveAddresses = { ...additiveAddresses, ...keysToAdd };
});
}
...
}
return (
...
button onClick={() => addSection(form)}
button onClick={() => removeSection(form)}
)
}

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).

How to proper setup setTimeout with recursively using React hooks?

I'm building a component that should show elements of an array, one per time and it shows the element for a period of time determined by the data. With the data below I would like to show textA for 5 seconds and then change to textB which I would show for 20 seconds, and then show textC for 10 seconds and start over again, showing textA and so on.
Here is what I'm trying:
const slides = [
{text: 'textA, time_length: 5},
{text: 'textB', time_length: 20},
{text: 'textC', time_length: 10}
] ;
const play = () => {
if (slides && slides.length > 0) {
const playSlides = () => {
this.timer = setTimeout(() => {
// update index
if (currentIndex + 1 < splashes.length) {
setCurrentIndex(currentIndex + 1)
} else {
setCurrentIndex(0)
}
}, slides[currentIndex])
}
}
}
useEffect(() => {
if (slides.length) {
debugger
play(slides)
}
return clearTimeout(this.timer)
}, [slides])
return <p>{slides[currentIndex]}</p>
}
This solution will show the slide.text every slide.time_length.
Notice that the 'loop' is achieved via the dependency [currentIndex]; in other words, every time the currentIndex changes, the useEffect will run once more, and will update the currentIndex after the next time_length seconds.
The calculation (currentIndex + 1) % slides.length will reset the index back to 0 if it overflows the array length (it's just a shortened version of your conditional, nothing special).
const slides = [
{ text: 'textA', time_length: 5 },
{ text: 'textB', time_length: 20 },
{ text: 'textC', time_length: 10 }
];
const TextSlider = () => {
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
if (slides.length) {
const timeoutId = setTimeout(() => {
setCurrentIndex((currentIndex + 1) % slides.length)
}, slides[currentIndex].time_length * 1000)
return () => clearTimeout(timeoutId);
}
return () => { }
}, [currentIndex])
return <h1>{slides[currentIndex].text}</h1>
}
Sometimes being less wordy can make it easier to see what's going on. Your mileage may vary -
import { useState, useEffect } from "react"
function TextSlider ({ slides = [] }) {
const [i, nextSlide] =
useState(0)
useEffect(() => {
if (i >= slides.length) return
const t =
setTimeout
( nextSlide
, slides[i].time_length * 1000
, (i + 1) % slides.length
)
return () => clearTimeout(t)
}, [i])
return <h1>{slides[i].text}</h1>
}
ReactDom.render(document.body, <TextSlider slides={slides} />)

Modify React state variable after specified time

I want to delete some entries from an array after a specified amount of time. notifications should be changed immediately, but in practice it remains unchanged. Is there a way to achieve my goal?
const Notifications = props => {
const [notifications, setNotifications] = useState([
{ value: '123', time: Date.now() },
{ value: '456', time: Date.now() }
]);
useEffect(() => {
let interval = setInterval(() => {
console.log(notifications);
let time = Date.now();
let array = notifications.filter(function(item) {
return time < item.time + 0;
});
setNotifications(array);
}, 500);
return () => {
clearInterval(interval);
};
}, []);
return null
}
Your code is fine! the problem is where you're logging it.
All state updates are asynchronous. With hooks the updated value is exposed in the scope of your component only in the next render call. If you log outside your effect you should see the correct values
const Notifications = props => {
const [notifications, setNotifications] = useState([
{ value: '123', time: Date.now() },
{ value: '456', time: Date.now() }
]);
useEffect(() => {
let interval = setInterval(() => {
let time = Date.now();
let array = notifications.filter(function(item) {
return time < item.time + 0;
});
setNotifications(array);
}, 500);
return () => {
clearInterval(interval);
};
}, []);
return null
}
console.log(notifications);

Resources