State variable hook does not increment within closure - reactjs

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

Related

Smooth animation of the search bar and the dropdown list along with it

I'm trying to achieve smooth animation of searchbar (MUI Autocomplete). This should work only on Smartphones (Screen < 600px).
Here is an example (it is very buggy and open it on smartphone to see the animation): https://react-zxuspr-gjq5w8.stackblitz.io/
And here is my implementation, but I've a few problems with that:
The interval does not reset on dropdown close.
The React.useEffect() dependency is set to searchActive, which is changed dynamically.
I tried calling the callback function of React.useState(), but since the component is not destroyed, I am not sure if it makes sense.
The width of dropdown, which is also changed in the setInterval() function, is not smooth at all.
https://stackblitz.com/edit/react-zxuspr-gjq5w8?file=demo.js
Here is part of the component where the logic is implemented:
export function PrimarySearchAppBar() {
const [searchActive, setSearchActive] = React.useState(null);
const [acPaperWidth, setAcPaperWidth] = React.useState(null);
const [acPaperTransX, setAcPaperTransX] = React.useState(0);
const AcRef = React.useRef(null);
const isMobile = useMediaQuery(useTheme().breakpoints.down('sm'));
const options = top100Films.map((option) => {
const group = option.group.toUpperCase();
return {
firstLetter: group,
...option,
};
});
React.useLayoutEffect(() => {
if (AcRef.current) {
setAcPaperWidth(AcRef.current.offsetWidth);
}
console.log(acPaperWidth);
}, [AcRef]);
let interval;
React.useEffect(() => {
if (searchActive) {
if (acPaperTransX <= 39) {
interval = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
setAcPaperWidth((acPaperTransX) => acPaperTransX + 1);
if (acPaperTransX >= 38) {
clearInterval(interval);
}
console.log(acPaperTransX);
}, 10);
}
} else {
setAcPaperTransX(0);
clearInterval(interval);
}
}, [searchActive]);
return (
<>Hello World</>
);
}
The problem was that the interval variable was defined outside the React.useEffect() hook, so its value was not preserved on re-renders.
I was able to fix that by using React.useRef():
const intervalRef = React.useRef();
const acPaperTransXRef = React.useRef(acPaperTransX);
React.useEffect(() => {
if (searchActive) {
if (acPaperTransXRef.current <= 39) {
intervalRef.current = setInterval(() => {
setAcPaperWidth(AcRef.current.offsetWidth);
acPaperTransXRef.current += 0.1;
if (acPaperTransXRef.current >= 38) {
clearInterval(intervalRef.current);
acPaperTransXRef.current = 0;
}
console.log(acPaperTransXRef.current);
}, 2);
}
} else {
setAcPaperTransX(0);
acPaperTransXRef.current = 0;
clearInterval(intervalRef.current);
}
return () => clearInterval(intervalRef.current);
}, [searchActive]);

react-countdown is not reseting or re-rendering second time

What I am trying to do is to update the reset the countdown after changing the status.
There are three status that i am fetching from API .. future, live and expired
If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.
So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.
import React, { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
return Object.keys(formatted).map((key) => {
return (
<div key={`${key}`} className={`countDown bordered ${key}-box`}>
<span className={`num item ${key}`}>{formatted[key]}</span>
<span>{key}</span>
</div>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
You use auctionStatus as a dependency for useEffect.
And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.
For answering your comment on how to resolve the issue..
I am not sure of your logic but I'll explain by this simple example.
export function App() {
// set state to 'live' by default
const [auctionStatus, setAuctionStatus] = React.useState("live")
React.useEffect(() => {
console.log('hello')
changeState()
}, [auctionStatus])
function changeState() {
// This line won't result in calling your useEffect
// setAuctionStatus("live") // 'hello' will be printed one time only.
// You need to use a state value that won't be similar to the previous one.
setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
}
}
You can simply use a flag instead that will keep on changing from true to false like this:
const [flag, setFlag] = React.useState(true)
useEffect(() => {
// ..
}, [flag])
// And in handleRenderer
getAuctionStatus().then((response) => {
setFlag(!flag);
});
Have a look at the following useCountdown hook:
https://codepen.io/AdamMorsi/pen/eYMpxOQ
const DEFAULT_TIME_IN_SECONDS = 60;
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};

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

React update or add to an array of objects in useState when a new object is received

Occasionally a newItem is received from a WebSocket and gets saved to useState with saveNewItem
this then kicks off the useEffect block as expected.
Update. If there is an object in the closeArray with the same openTime as the newItem I want to replace that object in closeArray with the newItem because it will have a new close
Add. If there isn't an object in the closeArray with the same open time as newItem I want to push the new item into the array.
Remove. And finally, if the array gets longer than 39 objects I want to remove of the first item.
If I add closeArray to the array of useEffect dependencies I'm going to create a nasty loop, if I don't add it closeArray isn't going to get updated.
I want usEffect to only fire off when newItem changes and not if closeArray changes, but I still want to get and set data to closeArray in useEffect
interface CloseInterface {
openTime: number;
closeTime: number;
close: number;
}
function App() {
const [newItem, saveNewItem] = useState<CloseInterface>();
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
useEffect(() => {
if (newItem) {
let found = false;
let arr = [];
for (let i = 0; i < closeArray.length; i++) {
if (closeArray[i].openTime === newItem.openTime) {
found = true;
arr.push(newItem);
} else {
arr.push(closeArray[i]);
}
}
if (found === false) {
arr.push(newItem)
}
if (arr.length === 39) arr.shift();
saveCloseArray(arr);
}
}, [newItem]); // <--- I need to add closeArray but it will make a yucky loop
If I do add closeArray to the useEffect dependancy array I get the error...
index.js:1 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
in App (at src/index.tsx:9)
in StrictMode (at src/index.tsx:8)
if I don't add closeArray to the useEffect dependancy array I get this error...
React Hook useEffect has a missing dependency: 'closeArray'. Either include it or remove the dependency array react-hooks/exhaustive-deps
the second useEffect block gets the initial data for closeArray and listens to a WebSocket that updates newItem as it arrives.
useEffect(() => {
const getDetails = async () => {
const params = new window.URLSearchParams({
symbol: symbol.toUpperCase(),
interval
});
const url = `https://api.binance.com/api/v3/klines?${params}&limit=39`;
const response = await fetch(url, { method: "GET" });
const data = await response.json();
if (data) {
const arrayLength = data.length;
let newcloseArray = [];
for (var i = 0; i < arrayLength; i++) {
const openTime = data[i][0];
const closeTime = data[i][6];
const close = data[i][4];
newcloseArray.push({ openTime, closeTime, close });
}
saveCloseArray(newcloseArray);
const ws = new WebSocket("wss://stream.binance.com:9443/ws");
ws.onopen = () =>
ws.send(
JSON.stringify({
method: "SUBSCRIBE",
params: [`${symbol}#kline_${interval}`],
id: 1
})
);
ws.onmessage = e => {
const data = JSON.parse(e.data);
const value = data.k;
if (value) {
const openTime = value.t;
const closeTime = value.T;
const close = value.c;
saveNewItem({ openTime, closeTime, close });
}
};
}
};
getDetails();
}, [symbol, interval]);
In order to better write your code, you can make use of state updater callback method, so that even if you don't pass closeArray to the useEffect and it will sstill have updated values on each run of useEffect
function App() {
const [newItem, saveNewItem] = useState<CloseInterface>();
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
useEffect(() => {
if (newItem) {
let found = false;
saveCloseArray(prevCloseArray => {
let arr = [];
for (let i = 0; i < prevCloseArray.length; i++) {
if (prevCloseArray[i].openTime === newItem.openTime) {
found = true;
arr.push(newItem);
} else {
arr.push(prevCloseArray[i]);
}
}
if (found === false) {
arr.push(newItem)
}
if (arr.length === 39) arr.shift();
return arr;
})
}
}, [newItem]);
You want to use a useCallback to save your new array with the updated item, like so:
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
const updateEntry = useCallback(newItem => {
saveCloseArray(oldCloseArray => oldCloseArray.reduce((acc, item) => {
acc.push(item.openTime === newItem.openTime ? newItem : item);
return acc;
}, []));
}, []);
You'd then apply the callback function to your button or div or whatever component is being generated, EG
return (
[1, 2, 3, 4, 5].map(item => <button key={`${item}`} onClick={() => updateEntry(item)}>Click me</button>)
);
If the only reason you have newItem is to update closeArray I would consider moving that functionality into the useEffect that utilizes WebSocket. You can still use newItem if you need to do something in addition to just updating closeArray, like showing an alert or a popup, for instance. Here's what I mean:
interface CloseInterface {
openTime: number;
closeTime: number;
close: number;
}
function App() {
const [newItem, saveNewItem] = useState<CloseInterface>();
const [closeArray, saveCloseArray] = useState<CloseInterface[]>([]);
useEffect(() => {
// Do something when newItem changes, e.g. show alert
if (newItem) {
}
}, [newItem]);
useEffect(() => {
// Work with the new item
const precessNewItem = (item = {}) => {
let found = false;
let arr = [];
for (let i = 0; i < closeArray.length; i++) {
if (closeArray[i].openTime === item.openTime) {
found = true;
arr.push(item);
} else {
arr.push(closeArray[i]);
}
}
if (found === false) {
arr.push(item)
}
if (arr.length === 39) arr.shift();
saveCloseArray(arr);
// save new item
saveNewItem(item);
};
const getDetails = async () => {
const params = new window.URLSearchParams({
symbol: symbol.toUpperCase(),
interval
});
const url = `https://api.binance.com/api/v3/klines?${params}&limit=39`;
const response = await fetch(url, { method: "GET" });
const data = await response.json();
if (data) {
const arrayLength = data.length;
let newcloseArray = [];
for (var i = 0; i < arrayLength; i++) {
const openTime = data[i][0];
const closeTime = data[i][6];
const close = data[i][4];
newcloseArray.push({ openTime, closeTime, close });
}
saveCloseArray(newcloseArray);
const ws = new WebSocket("wss://stream.binance.com:9443/ws");
ws.onopen = () =>
ws.send(
JSON.stringify({
method: "SUBSCRIBE",
params: [`${symbol}#kline_${interval}`],
id: 1
})
);
ws.onmessage = e => {
const data = JSON.parse(e.data);
const value = data.k;
if (value) {
const openTime = value.t;
const closeTime = value.T;
const close = value.c;
// process new item
processNewItem({ openTime, closeTime, close });
}
};
}
};
getDetails();
}, [symbol, interval, closeArray]); // add closeArray here
}

Resources