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
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]);
})
})
}
The state doesn't update the value even though I'm setting it to the oldvalue + 1.
When logging out the values of ltrNewValue or rtlNewValue it's always the same. It's as it's being overwritten by the initial state.
const Row = (props) => {
const [rowState, setRowState] = useState({
renderInterval: null,
value: 0,
});
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
My end goal is to increment the rowState.value up to 2 and then setting it to 0 in a infinite loop. How do I do this correctly?
I'm not certain, but it looks like you have a problem with a stale callback here.
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, []);
This effect only runs once - When the component is mounted the first time. It uses the counterIntervalFunction function for the interval:
const counterIntervalFunction = () => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = rowState.value === 2 ? 0 : rowState.value + 1;
console.log(ltrNewValue); // always 1
setRowState({ ...rowState, value: ltrNewValue });
console.log(rowState.value); // always 0
props.setRotatingValue(props.index, rowState.value);
} else if (props.isRunning && props.direction === 'rtl') {
const rtlNewValue = rowState.value === 0 ? 2 : rowState.value - 1;
setRowState({ ...rowState, value: rtlNewValue });
props.setRotatingValue(props.index, rowState.value);
} else {
clearCounterInterval();
}
};
The counterIntervalFunction captures the reference of props and uses it to determine what to display to the user. However, because this function is only run when the component is mounted, the event will only be run with the props passed to the function originally! You can see an example of this happening in this codesandbox.io
This is why you should put all external dependencies inside of the dependencies array:
useEffect(() => {
const interval = setInterval(counterIntervalFunction, props.speed);
setRowState({ ...rowState, renderInterval: interval });
}, [counterIntervalFunction, props.speed, rowState]);
However, this will cause an infinite loop.
Setting state in useEffect is usually considered a bad idea, because it tends to lead to infinite loops - changing the state will cause the component to re-render, causing another effect to be triggered etc.
Looking at your effect loop, what you're actually interested in is capturing a reference to the interval. This interval won't actually have any impact on the component if it changes, so instead of using state, we can use a ref to keep track of it. Refs don't cause re-renders. This also means we can change value to be a stand-alone value.
Because we now no longer depend on rowState, we can remove that from the dependencies array, preventing an infinite render. Now our effect only depends on props.speed and counterIntervalFunction:
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.speed);
return () => {
cancelInterval(renderInterval.current);
};
}, [props.speed, counterIntervalFunction]);
This will work, but because counterIntervalFunction is defined inline, it will be recreated every render, causing the effect to trigger every render. We can stablize it with React.useCallback(). We'll also want to add all the dependencies of this function to ensure that we don't capture stale references to props and we can change setRowState to setValue. Finally, because the interval is cancelled by useEffect, we don't need to call clearCounterInterval anymore.
const counterIntervalFunction = React.useCallback(() => {
if (props.isRunning && props.direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
props.setRotatingValue(props.index, ltrNewValue);
} else if (isRunning && props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
props.setRotatingValue(props.index, rtlNewValue);
}
}, [value, props]);
This can be simplified even further by moving the required props to the arguments:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
This could be even simpler if not for setRotatingValue: Right now, you have a component that both maintains it's own state and tells the parent when its state changes. You should be aware that the component state value might not necessarily update when you call it, but setRotatingValue absolutely will. This may lead to a situation where the parent sees a different state than the child does. I would recommend altering the way your data flows such that it's the parent that owns the current value and passes it via props, not the child.
This gives us the following code to finish off:
function Row = (props) => {
const renderInterval = React.useRef();
const [value, setValue] = React.useState(0);
useEffect(() => {
renderInterval.current = setInterval(counterIntervalFunction, props.isRunning, props.direction, props.setRotatingValue, props.index);
return () => {
cancelInterval(renderInterval.current);
};
}, [props, counterIntervalFunction]);
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
if (direction === 'ltr') {
const ltrNewValue = value === 2 ? 0 : value + 1;
setValue(ltrNewValue);
setRotatingValue(index, ltrNewValue);
} else if (props.direction === 'rtl') {
const rtlNewValue = value === 0 ? 2 : value - 1;
setValue(rtlNewValue);
setRotatingValue(index, rtlNewValue);
}
}, [value]);
...
}
In this code, you'll notice that we run the effect every time the props or the function changes. This will mean that, unfortunately, the effect will return every loop, because we need to keep a fresh reference to value. This component will always have this problem unless you refactor counterIntervalFunction to not notify the parent with setRotatingValue or for this function to not contain its own state. An alternatively way we could solve this would be using the function form of setValue:
const counterIntervalFunction = React.useCallback((isRunning, direction, setRotatingValue, index) => {
if (isRunning === false) {
return;
}
setValue(value => {
if (direction === 'ltr') {
return value === 2 ? 0 : value + 1;
} else if (direction ==' rtl') {
return value === 0 ? 2 : value - 1;
}
})
}, []);
Because the state update is not guaranteed to run synchronously, there's no way to extract the value from the setValue call and then call the setRotatingValue function, though. :( You could potentially call setRotatingValue inside of the setValue callback but that gives me the heebie geebies.
It's an interval and it may mess things up when you call setState directly by relying on the old state by the name rowState, try this:
setRowState(oldstate=> { ...rowState, value: oldstate.value+1 });
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.
Component will preserve state even after unmounting it
I'm building a feedback form with Formik and want to move from class components to hooks but face mentioned difficulties.
function Feedback(props) {
const [fileInfo, setFileInfo] = useState("");
const [feedbackStatus, setFeedbackStatus] = useState("");
let timer = null;
useEffect(() => {
const status = props.feedbackResponse.status;
if (status) {
if (status >= 200 && status < 300) {
setFeedbackStatus("success");
timer = setTimeout(() => {
props.history.goBack();
}, 2500);
} else if (status === "pending") {
setFeedbackStatus("pending");
} else {
setFeedbackStatus("error");
}
}
return () => {
clearInterval(timer);
}
}, [props.feedbackResponse.status]);
// ...code ommited for brevity
}
This effect runs succesfully after my form submission while waiting for a server response. Feedback component is a react-router modal component, if it matters. However, if I re-open that modal, I see a success message instead of a new form. In my return I am conditionally rendering a success message if feedbackStatus === "success" or a form that, depending on a server response, might display an error message otherwise. My class component works fine with this code:
componentDidUpdate(prevProps) {
const status = this.props.feedbackResponse.status;
if (prevProps.feedbackResponse.status !== status) {
if (status >= 200 && status < 300) {
this.setState({feedbackStatus: "success"});
this.timer = setTimeout(() => {
this.props.history.goBack();
}, 2500);
} else if (status === "pending") {
this.setState({feedbackStatus: "pending"});
} else {
this.setState({feedbackStatus: "error"});
};
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
Expected output: reopening this modal component after a successful form submit should render a new form but it renders a previous' submit status. This leads me to think that I'm not unmounting my Feedback component at all but where's my mistake then?
You can use <Feedback key={someKey} />.
This will ensure that a new instance of Feedback component is made when you re-open it, thus your old success/failure messages will be erased from the state.
The above behaviour happens because the effect is run on initial render as well and in that case props.feedbackStatus might be preserved from the previous instances.
Since you only wish to execute the effect when the component updates, you would need to stop execution of useEffect on initial render which happens even when you pass values to the dependency array. You can do that using useRef
function Feedback(props) {
const [fileInfo, setFileInfo] = useState("");
const [feedbackStatus, setFeedbackStatus] = useState("");
const isInitialRender = useRef(true);
let timer = null;
useEffect(() => {
if(isInitialRender.current === true) {
isInitialRender.current = false;
} else {
const status = props.feedbackResponse.status;
if (status) {
if (status >= 200 && status < 300) {
setFeedbackStatus("success");
timer = setTimeout(() => {
props.history.goBack();
}, 2500);
} else if (status === "pending") {
setFeedbackStatus("pending");
} else {
setFeedbackStatus("error");
}
}
}
return () => {
clearInterval(timer);
}
}, [props.feedbackResponse.status]);
}