Use effect doesn't see state - reactjs

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

Related

React state's previous value equals new value with setInterval

I am setting up a setInterval with some VideoPlayer iFrame. Every 10 seconds or so, I need to call an API to update back-end with new state of the video played.
I want to have another check on client, if the data from previous state is same as new one (This case occurs when the video is paused for longer duration) I don't want to call the back-end API.
const [coveredArray, setCoveredArray] = useState([]);
const intervalPeriod = 5000;
const interval = useRef();
useEffect(() => {
if (typeof window !== "undefined" && isOpen && player) {
console.log('SUB');
// Setup an interval to update watch time
interval.current = window.setInterval(async () => {
const totalCoveredArray = await player?.api.getTotalCoveredArray();
setCoveredArray((prevCoveredArray) => {
console.log('1', prevCoveredArray);
console.log('2', totalCoveredArray);
if (!isEqual(prevCoveredArray, totalCoveredArray)) {
console.log('New');
return totalCoveredArray;
} else {
console.log('Same');
return prevCoveredArray;
}
});
}, intervalPeriod);
return () => {
// Clear interval on exit
console.log('UN-SUB')
setCoveredArray([]);
return window.clearInterval(interval.current)
};
}
}, [isOpen, player]);
The problem with this code is, prevCoveredArray and totalCoveredArray are same everytime the interval is run. How is previousState similar to newState (which is not set yet)?
This is logged in console:
Any help/pointer would be helpful, thanks.

How do I reference previous state in lodash's filter method?

const [panelRight, setPanelRight] = useState(sample(filter(menu, (el) => el !== panelLeft)));
useEffect(() => {
const interval = setInterval(() => {
const menuCategory = filter(menu, (el) => el !== panelLeft && el !== panelRight);
setPanelRight(sample(menuCategory));
}, 10000);
Currently panelRight is being set to the same random menu element every time the the setInterval is called. I think this might be because I need to reference the previous state in the filter method, but how?
Your code lacks details, we dont know what is in your useEffect deps so lets assume and guess. Provide more details next time, please.
Lets assume your useEffect has an empty [] dependencies array and called only once on the first render.
So the issue - setInterval will always hold the value it had initially when the setInterval callback (closure) was defined. Values from the first render.
And here is what you should do:
// Insert that before or after your setStates section
const panelRightRef = React.useRef();
const panelLeftRef = React.useRef();
// That will update ref variables
useEffect(() => {panelRightRef.current = panelRight}, [panelRight]);
useEffect(() => {panelLeftRef.current = panelLeft}, [panelLeft]);
useEffect(() => {
const interval = setInterval(() => {
// if menu is also a State variable and is not immutable - you should add useRef() for it
const menuCategory = filter(menu, (el) => el !== panelLeftRef.current && el !== panelRightRef.current);
setPanelRight(sample(menuCategory));
}, 10000);
// that will be called on depsarray changes or when component is destroyed
return () => clearInterval(interval);
}, []);

reactjs - update setState with array values

I want to update the setState in realtime but it is giving me hard time since state update is an asynchronous process. May I request for your assistance so I can update state by the time button was clicked.
Here's my code:
const [dependentSystems, setdependentSystems] = useState([]);
const getDependentSystems = async() => {
const response = await axios.get('/GETAPI' + selectedSysID.SYSTEMID)
console.log("LIST OF DEPENDENT SYSTEM", response.data)
setdependentSystems(response.data)
}
JSX part
<IconButton>
<Icon
onClick={() => selectedSystem(row,'AddDep')}
/>
<Icon>
<IconButton>
selectedSystem
const [selectedSystemID, setselectedSystemID] = useState('');
let selectedSysID;
const selectedSystem = (row,action) =>{
selectedsysID = {...selectedSystemID, 'SYSTEMID':row.SYSTEMID}
getDependentSystems();
(action === 'AddDep') ? openModal() : openOtherModal()
}
Here's the result of console.log
I want to save the result of response.data in array on the first trigger or call of getDependentSystems in short in realtime so I can display in modal the dependent system by the time Edit button was clicked. What is happening is I need to close again the modal then edit again to display the dependent systems
Hope you can help me with this. Thank you
Trying to open the modal and expecting to see the state value you just enqueued won't ever work, due to the way React asynchronously processed enqueued state updates.
What I suggest then is to place the modal open triggering into a setTimeout call so that the function can complete and allow React to process the enqueued state update. Just enough to get the timeout callback to execute on a tick after the state update was processed, just about any timeout should be sufficient, but this is a bit hackish and obviously you will want to fine-tune this specifically to your app.
const selectedSystem = (row, action) => {
selectedsysID = {...selectedSystemID, 'SYSTEMID':row.SYSTEMID}
getDependentSystems();
setTimeout(() => {
(action === 'AddDep') ? openModal() : openOtherModal();
}, 17); // delay by about 1 minimum render cycle
};
An alternative would be to store the action in state and use an useEffect hook to issue the side-effect up opening the modal.
const [savedAction, setSavedAction] = useState();
const [dependentSystems, setDependentSystems] = useState([]);
const getDependentSystems = async (action) => {
const response = await axios.get('/GETAPI' + selectedSysID.SYSTEMID);
console.log("LIST OF DEPENDENT SYSTEM", response.data);
setDependentSystems(response.data);
setSavedAction(action);
};
useEffect(() => {
if (savedAction) {
(action === 'AddDep') ? openModal() : openOtherModal();
setSavedAction(null);
}
}, [savedAction]);
const selectedSystem = (row, action) => {
selectedsysID = {...selectedSystemID, 'SYSTEMID':row.SYSTEMID}
getDependentSystems(action);
};
Just stick the getDependentSystems as a onClick handler of a button.
const [dependentSystems, setdependentSystems] = useState([]);
const getDependentSystems = async () => {
const response = await axios.get('/GETAPI' + SYSTEMID)
console.log("LIST OF DEPENDENT SYSTEM", response.data)
setdependentSystems(response.data)
}
On the JSX part:
return <button onClick={getDependentSystems}>GET SYSTEM</button>

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

ReactJS Use SetInterval inside UseEffect Causes State Loss

So I am writing a product prototype in create-react-app, and in my App.js, inside the app() function, I have:
const [showCanvas, setShowCanvas] = useState(true)
This state is controlled by a button with an onClick function; And then I have a function, inside it, the detectDots function should be ran in an interval:
const runFaceDots = async (key, dot) => {
const net = await facemesh.load(...);
setInterval(() => {
detectDots(net, key, dot);
}, 10);
// return ()=>clearInterval(interval);};
And the detectDots function works like this:
const detectDots = async (net, key, dot) => {
...
console.log(showCanvas);
requestFrame(()=>{drawDots(..., showCanvas)});
}
}};
I have a useEffect like this:
useEffect(()=>{
runFaceDots(); return () => {clearInterval(runFaceDots)}}, [showCanvas])
And finally, I can change the state by clicking these two buttons:
return (
...
<Button
onClick={()=>{setShowCanvas(true)}}>
Show Canvas
</Button>
<Button
onClick={()=> {setShowCanvas(false)}}>
Hide Canvas
</Button>
...
</div>);
I checked a few posts online, saying that not clearing interval would cause state loss. In my case, I see some strange behaviour from useEffect: when I use onClick to setShowCanvas(false), the console shows that console.log(showCanvas) keeps switching from true to false back and forth.
a screenshot of the console message
you can see initially, the showCanvas state was true, which makes sense. But when I clicked the "hide canvas" button, and I only clicked it once, the showCanvas was set to false, and it should stay false, because I did not click the "show canvas" button.
I am very confused and hope someone could help.
Try using useCallback for runFaceDots function - https://reactjs.org/docs/hooks-reference.html#usecallback
And ensure you return the setInterval variable to clear the timer.
const runFaceDots = useCallback(async (key, dot) => {
const net = await facemesh.load(...);
const timer = setInterval(() => {
detectDots(net, key, dot);
}, 10);
return timer //this is to be used for clearing the interval
},[showCanvas])
Then change useEffect to this - running the function only if showCanvas is true
useEffect(()=>{
if (showCanvas) {
const timer = runFaceDots();
return () => {clearInterval(timer)}
}
}, [showCanvas])
Update: Using a global timer
let timer // <-- create the variable outside the component.
const MyComponent = () => {
.....
useEffect(()=>{
if (showCanvas) {
runFaceDots(); // You can remove const timer here
return () => {clearInterval(timer)}
} else {
clearInterval(timer) //<-- clear the interval when hiding
}
}, [showCanvas])
const runFaceDots = useCallback(async (key, dot) => {
const net = await facemesh.load(...);
timer = setInterval(() => { //<--- remove const and use global variable
detectDots(net, key, dot);
}, 10);
return timer //this is to be used for clearing the interval
},[showCanvas])
.....
}

Resources