state hooks don't update from useEffect function calls - reactjs

In useEffect I call getPins which queries a DB for pins. I am then looping through the pins and and trying to decrease 'pinsRemaining' which is a state hook.
In state it shows that the hook decreases, but it's not working in the code. When I console log pinsRemaining from getPinnedLocations() its always the same number.
The interesting thing is that if I make 'pinsRemaining' a const varaible (not state hook) it does work to decrease the counter, but that creates other problems.
I'm not sure how to get this code to work. I've read about how state hooks are async here
useState set method not reflecting change immediately
I've tried just adding an empty useEffect hook like this but it doesn't work
useEffect(() => {}, [pinsRemaining])
Maybe I need to be doing something inside of this?
Here is my full code.
const [pinsRemaining, setPinsRemaining] = useState(5)
useEffect(() => {
if (isLoggedIn && user !== {}) {
APIService.getAccountById(
Number(id)
).then(_user => {
setUser(_user)
getPins(_user)
}).catch(err => {
console.log("error", err)
})
}
}, []);
const getPins = (_user) => {
APIService.getPinsForUser(_user.id).then(pins => {
pins.map(pin => {
pushPinnedLocation(pin.location_id, _user)
})
})
}
const pushPinnedLocation = (location, date, _user) => {
//decrementing in hook doesn't work here
setPinsRemaining((prevState)=> prevState - 1)
//but decrementing with var does
pins = pins - 1
console.log("state hook", pinsRemaining)
console.log("const var", pins)
setLocationDateMap(new Map(locationDateMap.set(location, date)))
if(pinsRemaining === 0){
getMatchesFromDB(_user)
}
}
const getMatchesFromDB = (_user) => {
let pinsArray = Array.from(locationDateMap)
pinsArray.map(obj => {
let location = obj[0]
let date = obj[1]
let locationDate = date + "-" + location
let dbMatches;
let params = {
location_date_id: locationDate,
user_id: _user.id,
seeking: _user.seeking
}
APIService.getMatchesByLocationDateId(params).then(data => {
dbMatches = data.map((match, i) => {
if(match.likes_recieved && match.likes_sent
&& match.likes_recieved.includes(_user.id + "")
&& match.likes_sent.includes(_user.id + "")){
match.connectedStatus = 3
}else if(match.likes_recieved && match.likes_recieved.includes(_user.id + "")){
match.connectedStatus = 2
}else if(match.likes_sent && match.likes_sent.includes(_user.id + "")){
match.connectedStatus = 1
}else{
match.connectedStatus = 0
}
match.name = match.user_name
return match
})
}).then(() => {
setMatches(matches => [...matches, ...dbMatches]);
})
})
}

Related

useRef value doesn't update in useEffect

I update a useRef.current value in an useEffect, and I want to use that value in another useEffect. But then I find that it doesn't get the updated value, even though I console it in global and make sure it do updated.
const articleAmountRef = useRef<number>(0);
useEffect(() => {
if (windowResized === "small" || windowResized === undefined) return;
let isFetching = false;
let isPaging = true;
let paging = 0;
const el = scrollRef.current;
setArticles([]);
async function queryNews(input: string) {
isFetching = true;
setIsLoading(true);
setScrolling(false);
setSearchState(true);
setPageOnLoad(true);
const resp = await index.search(`${input}`, {
page: paging,
});
const hits = resp?.hits as ArticleType[];
/////////Change useRef value here///////////
articleAmountRef.current = resp?.nbHits;
setArticles((prev) => [...prev, ...hits]);
setIsLoading(false);
paging = paging + 1;
if (paging === resp?.nbPages) {
isPaging = false;
setScrolling(true);
return;
}
isFetching = false;
setScrolling(true);
setSearchState(false);
setPageOnLoad(false);
}
async function scrollHandler(e: WheelEvent) {
if (el!.scrollWidth - (window.innerWidth + el!.scrollLeft) <= 200) {
if (e.deltaY < 0 || isFetching || !isPaging) return;
queryNews(keyword);
}
}
queryNews(keyword);
el?.addEventListener("wheel", scrollHandler);
return () => {
el?.removeEventListener("wheel", scrollHandler);
};
}, [keyword, setArticles, setSearchState, windowResized]);
console.log(
"outside useEffect,articleAmountRef.current=",
articleAmountRef.current
);
/////////use updated useRef value in this useEffect///////////
useEffect(() => {
if (windowResized === "small" || windowResized === undefined) return;
blockWidth.current = newsBlockRef.current?.offsetWidth!;
console.log(
"inside useEffect,articleAmountRef.current=",
articleAmountRef.current
);
if (windowWidth >= 1280) {
contentLength.current =
Math.ceil(articleAmountRef.current / 2) * blockWidth.current +
Math.ceil(articleAmountRef.current / 2) * 60;
}
if (windowWidth < 1280) {
contentLength.current =
Math.ceil(articleAmountRef.current / 2) * blockWidth.current +
Math.ceil(articleAmountRef.current / 2) * 30;
}
}, [windowResized]);
console.log results looks like below:
useRef result
How can I make second useRef get updated value?I want second useRef has value "11476". I tried to write articleAmountRef.current as second useRef's dependency, but eslint warning me "Mutable values like 'articleAmountRef.current' aren't valid dependencies because mutating them doesn't re-render the component."
Since value "11476" is a result of fetch data, I want it update after fetch data getting back. And this value only needs to fetch once, it doesn't need to update every time component re-render, so I tried to save this value by useRef instead of uesState.
Thank you for your kindly help!

Update non state variable in React useEffect

I have an application the receives new data over a WebSocket every second. Each second I receive 10 to 15 messages that I need to store in and display. I am currently updating a state array each time I receive new data but the effect is that I re-render the screen 10 to 15 times per second.
What I want to achieve is to store the incoming data in an array but only update the screen once every second.
My approach that I can't get working is to create a non-state array that is updated when new data is received and copy that data to a state array every second with a timer.
This is the declaration of the state array:
const [boatData2, _setBoatData2] = useState({});
const boatDataRef = useRef(boatData2);
const setBoatData2 = (update) => {
boatDataRef.current = update;
_setBoatData2(update);
}
This is the hook code where the data is received:
useEffect(() => {
if (!ws.current) return;
ws.current.onmessage = e => {
setDataFlowing(true);
setDataAge(0);
setScreenUpdates(screenUpdates => screenUpdates + 1);
//console.log('New Data');
const message = JSON.parse(e.data);
if (message.updates && message.updates.values) {
message.updates[0].values.forEach(obj => {
let newPath = obj.path.split('.').join("");
const update = {
path: obj.path,
value: obj.value,
timestamp: message.updates[0].timestamp,
valid: true,
age: 0,
};
now = Date.parse(message.updates[0].timestamp);
setBoatData2({ ...boatDataRef.current, [newPath]: update });
});
}
};
}, []);
This is the code that runs every second:
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
let boatdata = boatData2;
//console.log(boatData3);
Object.values(boatdata).forEach(val => {
val.age = val.age + 1;
if (val.age > 30) {
val.valid = false;
}
});
setBoatData2(boatdata);
setDataAge(dataAge => dataAge + 1);
if (dataAge > 60) {
setDataFlowing(false);
}
}, 1000);
} else if (!isActive && seconds !== 0) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isActive, seconds, boatData2]);
You can do this with the help of useRef .
const messageRef = useRef([]);
This creates a object inside messageRef called current which we can mutate and mutating it will not trigger a re-render. Now your messageRef will be something like this
{
current: []
}
Now whenever you get the message from the websocket push the message into this ref as
messageRef.current.push(your message)
Now inside your function which updates the state after some xyz second . You can use this Ref to update the state
setYourMessages(messageRef.current);
messageRef.current = []; // do this after you state update call. Else you will be pushing duplicate messages into the state

Why is UI not updating after button click in React?

I'm working on something similar like Youtube like/dislike functionality. First getting result by it's id. Then checking and filtering and lastly doing put request to the database. But the problem is that 'likes' values in the UI changes only after page refresh. I tried using useState hook and manipulate the state when getting response from put request, without succeeding. Everything happens on button click.
Any advice is strongly appreciated.
My States
const [likes, setLikes] = useState([])
const [dislikes, setDislikes] = useState([])
Getting the review
const getDislikedReviewById = (id) => {
axios.get(`https://landlordstrapi.herokuapp.com/cool-project/${id}`)
.then((response) => {
handleDislike(response.data)
})
.catch(err => {
console.log(err);
});
}
Working with functionality
const handleDislike = (data) => {
const id = data.id
setDislikes([...data.comment_info.dislikes])
setLikes([...data.comment_info.likes])
const userToken = localStorage.getItem('user') || []
const checkIfLikeExists = likes.find(item => item === userToken)
const checkIfDislikeExists = dislikes.find(item => item === userToken)
if(checkIfLikeExists && checkIfLikeExists.length) {
setLikes(likes.filter(a => a !== userToken))
setDislikes(dislikes.push(userToken))
}
if(!checkIfDislikeExists && !checkIfLikeExists) {
setDislikes(dislikes.push(userToken))
}
JSON.stringify(dislikes)
updateDislikes(data, id, dislikes, likes)
}
When everything is done sending UPDATE request
const updateDislikes = (data, id, dislikes, likes) => {
axios.put(`https://landlordstrapi.herokuapp.com/cool-project/${id}`, {
comment_info: {
likes: likes,
dislikes: dislikes,
comment: data.comment_info.comment
}
})
.then(function(response){
console.log('saved successfully')
});
}
My like button
<Votes>
<GrLike style={{cursor: "pointer"}} onClick={() => getReviewById(id)} size={10}/>
<VoteValue>
{likes && likes.length ? likes.length : null}
</VoteValue>
</Votes>
Not sure if you want to update the state or set the data returned by the api. I assumed now you would like to set the data from the api. If you want to set the state you can do so by using the old state, so pass a function to the setState call
setDislikes((oldDislikes) => [...oldDislikes, userToken]);
and I would update the handleDislike function as so
const handleDislike = (data) => {
const {
id,
comment_info: { dislikes, likes }
} = data;
let newLikes = likes;
let newDislikes = dislikes;
const userToken = localStorage.getItem("user") || [];
const likeExists = likes.find((item) => item === userToken);
const dislikeExists = dislikes.find((item) => item === userToken);
if (likeExists) {
newLikes = newLikes.filter((a) => a !== userToken);
}
if ((!dislikeExists && !likeExists) || likeExists) {
newDislikes = [...dislikes, userToken];
}
// set new values
setDislikes(newDislikes);
setLikes(newLikes);
updateDislikes(data, id, newDislikes, newLikes);
};

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
}

Running a Function on Click Returns Stale State Values React Hooks

I am refactoring a class component into a functional component with React Hooks in an app that runs a specific function on click. The function references state values, but the state values in the function are stale, and it causes the app to crash.
I've seen similar questions on StackOverflow, but most of the onClick functions do only one thing, so their use of useRef or useCallback seem much easier to implement. How can I ensure that the checkAnswer function is using updated state values?
const Find = props => {
const [currentCountry, setCurrentCountry] = useState(null)
const [guesses, setGuesses] = useState(null)
const [questions, setQuestions] = useState([])
EDIT
The setCurrentCountry hook is called in the takeTurn function, which runs at the start of the game.
const takeTurn = () => {
!props.isStarted && props.startGame();
let country = getRandomCountry();
console.log(country)
setGuesses(prevGuess => prevGuess + 1)
setCurrentCountry(country)
console.log('setting currentCountry')
getAnswers(country)
let nodes = [...(document.getElementsByClassName("gameCountry"))];
nodes.forEach( node => {
node.removeAttribute("style")
})
if(questions && questions.length === 10){
console.log('opening modal')
props.handleOpen();
// alert("Congrats! You've reached the end of the game. You answered " + props.correct + " questions correctly and " + props.incorrect + " incorrectly.\n Thanks for playing");
console.log('ending game')
props.gameOver && endGame();
}
const getAnswers = (currentCountry) => {
console.log(currentCountry)
let answerQuestions;
if(questions){
answerQuestions = [...questions]
}
let question = {};
question['country'] = currentCountry;
question['correct'] = null;
let answers = [];
currentCountry && console.log(currentCountry.name);
console.log(currentCountry)
currentCountry && answers.push({
name: currentCountry.name.split(';')[0],
correct: 2
});
console.log(answers)
answerQuestions.push(question);
setQuestions(answerQuestions)
}
const checkAnswer = (e, country) => {
let checkquestions = questions;
let question = checkquestions.find(question => question.country === currentCountry);
let checkguesses = guesses;
console.log(e)
console.log(country)
console.log(currentCountry)
if(!props.isStarted){
return
}
if((country === currentCountry.name || country === currentCountry.name) || guesses === 4){
props.updateScore(3-guesses);
console.log(question);
if(guesses === 1){
question['correct'] = true;
}
checkguesses = null;
setTimeout(() => takeTurn(), 300);
} else {
question['correct'] = false;
checkguesses ++
if(guesses === 3){
getCountryInfo(e, currentCountry.name);
}
}
setGuesses(checkguesses)
props.handlePoints(questions);
}
The rendered data with the onClick:
<Geographies geography={data}>
{(geos, proj) =>
geos.map((geo, i) =>
<Geography
data-idkey={i}
onClick={((e) => checkAnswer(e, geo.properties.NAME_LONG))}
key={i}
geography={geo}
projection={proj}
className="gameCountry"
/>
)
}
</ Geographies>
</ZoomableGroup>
The app stalls because the state values for currentCountry are still being read as null.

Resources