Update non state variable in React useEffect - reactjs

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

Related

Firestore onSnapshot() and useEffect

so I am currently working on a project, where I need a realtime datastream of a pool object and a user object. As I am using Firebase as a BAAS, I tried to refactor my code with onSnapshot, as this seemed to minimize requests to the database.
Objective: On Mounting of the component, set a realtime Listener on the pool object, which changes the state which stores the poolData, everyTime there is a change. Based on this up to date pool data, run a calculation every Second, which also updates other states.
Status Quo: When I am rebuilding my project (nextjs) it works for a few instances but then an error occurs when trying to access the pool object Error occured in getting Pool Time TypeError: Cannot read properties of null
I was thinking that maybe it throws an error because the standard value for state is set as null, so in the useEffect hook I tried to conventionally fetch a snapshot of the data first, but this also didn't seem to work. Furthermore I am pretty sure I am messing something up regarding the intricacies of useEffect and onSnapshot.
I really appreciate your help and your input!
const router = useRouter();
const slug = router.query.slug;
const profile = `/profile/${auth.currentUser.uid}`
const [timerDisplay, setTimerDisplay] = useState(0);
const [finalTime, setFinalTime] = useState(0);
const [tokenState, setTokenState] = useState(0);
const [open, setOpen] = useState(true);
const [currentlyWinning, setCurrentlyWinning] = useState("Nobody");
const [poolData, setPoolData] = useState(undefined);
console.log(poolData);
useEffect(() => {
getFirstPoolSnap();
const unsubPoolListener = onSnapshot(doc(db, "pools", slug), (doc) => {
console.log(doc.data());
setPoolData(doc.data());
});
const unsubTokenListener = onSnapshot(
doc(db, "users", auth.currentUser.uid),
(doc) => {
setTokenState(doc.data().tokens);
}
);
const interval = setInterval(() => {
getPoolTime();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
function getPoolTime() {
try {
let localTime = Math.floor(Date.now() / 1000);
const gbTimeLeft =
poolData.expTimestamp.toMillis() / 1000 - localTime;
const finalTimeLeft =
poolData.finalTime.toMillis() / 1000 - localTime;
setOpen(poolData.open);
if (gbTimeLeft < 0 || finalTimeLeft < 0) {
setTimerDisplay(0);
} else {
setTimerDisplay(Math.floor(gbTimeLeft));
}
if (finalTimeLeft < 0 || gbTimeLeft < 0) {
setFinalTime(0);
} else {
setFinalTime(Math.floor(finalTimeLeft));
}
setCurrentlyWinning(poolData.lastSetterName);
} catch (error) {
console.log("Error occured in getting Pool Time", error);
}
}

Implantation of counter in react has a value of 0 when I reference in an empty useEffect

I took code based off this page and adjusted it. I want to time the amount of milliseconds the user is on the component so I want to log the counter value when the component unmounts aka the return statement of useffect/componentWillUnmount().
const [milliseconds, setMilliseconds] = useState(0);
const isActive = useState(true);
const logger = new logger(stuff);
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
});
useEffect(() => {
return () => {
console.log("empty useffect milliseconds:", milliseconds);
logger(milliseconds);
clearInterval(milliseconds)
};
}, []);
useEffect(() => {
let interval: NodeJS.Timeout = setInterval(() => {
}, 0);
interval = setInterval(() => {
setMilliseconds(milliseconds => milliseconds + 1000);
}, 1000);
console.log("interval:", interval);
console.log("interval milliseconds:", milliseconds);
}, [ milliseconds]);
I see the millisecond printout fine in the "interval milliseconds" console statement but the "empty useffect milliseconds:" always prints out 0. What am I doing wrong?
You can remember a mount timestamp and then calculate the difference.
useEffect(() => {
const mountedAt = Date.now();
return () => {
const mountedMilliseconds = Date.now() - mountedAt;
console.log(mountedMilliseconds);
};
}, []);
Side note 1: use an empty array as deps if you want to run function on mount only. If you do not pass [] deps, your initializeIcons effect will run with each re-render. Do it like this:
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
}, []);
Side note 2: first interval you create creates a memory leak, because it does nothing, and is never cleared.
Another problem you have is milliseconds dependency in useEffect, which registers new intervals after each milliseconds state change.

Use effect doesn't see state

i'm working on a continuous notification (every 3 seconds and max 1 min), until user accept pending orders.
This is my code.
usePrevious is custom hook that gives me previous value of state.
I don't mind why, when setTimeout executes clearInterval(), 'notification' is null.
This problem cause loop in sound.
[.....]
const [notification, setNotification] = useState(null)
const previousOrderCount = usePrevious(newOrderCount)
const previousPendingReservationsLength = usePrevious(
pendingReservationsLength
)
const isToneActive = useRef(false)
// sound notify for new orders
useEffect(() => {
if (
!isToneActive.current &&
newOrderCount > previousOrderCount &&
user?.profile?.role === "manager"
) {
// this prevent multiple triggers of sound
isToneActive.current = true
setNotification(setInterval(() => playNotification(), 3000))
setTimeout(() => {
clearInterval(notification)
isToneActive.current = false
}, 60000)
}
}, [
newOrderCount,
playNotification,
user,
previousOrderCount,
stopNotify,
notification,
setNotification,
])
[....]
I would use another React ref to hold a reference to the interval timer. The issue here is that React state updates are asynchronous, so the setNotification state update doesn't immediately update the notification state for the setTimeout callback that encloses the current null state value.
const notificationRef = React.useRef(null);
...
notificationRef.current = setInterval(() => playNotification(), 3000);
setTimeout(() => {
clearInterval(notificationRef.current);
notificationRef.current = null;
isToneActive.current = false;
}, 60000);

state hooks don't update from useEffect function calls

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

React useEffect: Why is my value undefined initially?

I'm trying to setup an onKeyPress event listener and I'm confused as to why the initial value is undefined and then the value I want. The data is added on mount (see x in console). Why am I unable to immediately capture it and instead get an initial undefined, especially since it clearly already exists in state?
useEffect(() => {
console.log('x', multipleChoice); <-- logs the array of objects
const handleKeyPress = ({ key }) => {
const index = Number(key) - 1;
if (key === '1') {
console.log(multipleChoice[index]); <-- logs undefined, then logs object
}
};
window.addEventListener('keydown', (e) => handleKeyPress(e));
return () => {
window.removeEventListener('keydown', (e) => handleKeyPress(e));
};
}, [allCards, currentCard, multipleChoice]);
LocalState
const [currentCard, setCard] = useState(0);
const [multipleChoice, setMultipleChoice] = useState([]);
// allCards is passed as a prop on page load from the parent
When the user guesses an answer correctly the currentCard is incremented by 1
UseEffect that sets multipleChoice
useEffect(() => {
const generateMultipleChoice = (words: Word[]) => {
const possibleAnswers = words.reduce(
(accum: Word[]) => {
while (accum.length < 4) {
// randomly select words from pool
const index = getRandomInt(0, allCards.length - 1);
const randomWord = allCards[index];
// verify current hand doesn't already have that word
if (!accum.includes(randomWord)) {
accum.push(randomWord);
}
}
return accum;
},
// default with the current card already in the hand
[allCards[currentCard]]
);
// return the hand with the matching card and (3) other cards from pool
return possibleAnswers;
};
const shuffledCards = shuffle(generateMultipleChoice(allCards));
setMultipleChoice(shuffledCards);
}, [allCards, currentCard]);
screenshot of console
This is it:
// initial state
const [multipleChoice, setMultipleChoice] = useState([]);
// therefore, initially, if index is any Number
console.log(multipleChoice[index]) // undefined
The object is returned, only until the calculation is finished...
useEffect(() => {
// ...
// this has to run before `multipleChoice` is updated to return that object
const shuffledCards = shuffle(generateMultipleChoice(allCards));
setMultipleChoice(shuffledCards);
}, [allCards, currentCard]);

Resources