Stop useEffect if a condition is met - reactjs

I have a useEffect set up how I thought would only run once on initial render but it continues to rerun.
This breaks a function that is supposed to set a piece of state to true if a condition is truthy and show appropriate UI.
This sort of works but then the useEffect runs again flicks back to false immediately. I am also using a use effect to check on first render if the condition is truthy and show appropriate UI if so.
Basically when setIsPatched is true I don't want the useEffect to rerun because it flicks it back to false and breaks the UI
Here is the function:
const [isPatched, setIsPatched] = useState<boolean>(false);
useEffect(() => {
getApplied(x);
}, []);
const getApplied = (x: any) => {
console.log(x);
if (x.Log) {
setIsPatched(true);
return;
} else {
setIsPatched(false);
}
};
I also pass getApplied() to child component which passes a updated data to the function for use in this parent component:
const updatePatch = async (id: string) => {
//check if patch already in db
const content = await services.data.getContent(id);
const infoToUpdate = content?.data[0] as CN;
if (!infoToUpdate.applyLog && infoToUpdate.type == "1") {
// add applyLog property to indicate it is patched and put in DB
infoToUpdate.applyLog = [
{ clID: clID ?? "", usID: usID, appliedAt: Date.now() },
];
if (content) {
await services.data
.updateX(id, content, usId)
.then(() => {
if (mainRef.current) {
setDisabled(true);
}
});
}
getApplied(infoToUpdate);
} else {
console.log("retrying");
setTimeout(() => updatePatch(id), 1000); // retries after 1 second delay
}
};
updatePatch(id);
}

Related

Problem when using useEffect to store app's open time into an array

I am having a problem that is when I change user's open time, the app works well in the first change, but from the second change, it will clear the array that store time data and then the process will restart from the beginning. If I don't put anything in useEffect dependency, the app works correctly, but I have to reload the app every time the date is changed.
Here is the code to deal with the open time data:
curStreakUpdate = (date: string) => {
const nearliestDateOpen = this.currentStreak[this.currentStreak.length - 1];
const dateNear: any = new Date(nearliestDateOpen);
const dateNow: any = new Date(date);
if (this.currentStreak.length === 0) {
this.currentStreak.push(date);
this.setLongestStreak(this.currentStreak.length);
this.totalPrays += 1;
} else {
this.checkNearliestOpenDate(dateNow, dateNear, date);
}
console.log(this.currentStreak);
};
checkNearliestOpenDate = (dateNow: any, dateNear: any, date: string) => {
if (dateNow - dateNear !== 0) {
if (dateNow - dateNear === 86400000) {
if (!this.currentStreak.includes(date)) {
this.currentStreak.push(date);
this.setLongestStreak(this.currentStreak.length);
}
} else {
this.currentStreak = [];
}
this.totalPrays += 1;
}
};
-Here is where I use useEffect hook to store open time whenever user open the app:
const {curStreakUpdate} = useStore().streakStore;
const dt = new Date('2022-09-14').toISOString().split('T')[0];
useEffect(() => {
AppState.addEventListener('change', (state) => {
if (state === 'active') {
curStreakUpdate(dt);
// AsyncStorage.clear();
}
});
}, [dt]);
Here is the output
Array [
"2022-09-15",
]
Array [
"2022-09-15",
]
Array [
"2022-09-15",
"2022-09-16",
]
Array []
Array [
"2022-09-16",
]
If I don't put anything in useEffect dependency, the app works correctly, but I have to reload the app every time the date is changed.
You don't have to reload the app with cmd-r, I should be enough when you close/terminate the app.
The operating system iOS/Android usually kill an app which is in background for longer time.
So there is no need to kill the app manually in the real world for most cases.
checkNearliestOpenDate = (dateNow: any, dateNear: any, date: string) => {
if (dateNow - dateNear !== 0) {
if (dateNow - dateNear === 86400000) { // I think this should be >= or <=
if (!this.currentStreak.includes(date)) {
this.currentStreak.push(date);
this.setLongestStreak(this.currentStreak.length);
}
} else {
this.currentStreak = [];
}
this.totalPrays += 1;
}
};
Additionally you should cleanup your listener otherwise you would add another listener the view get opened.
And you have to ensure you haven't already added a listener currently you would add a listener each time dt changed.
I don't care if you really need the useEffect at all for what you wanna implement.
useEffect(() => {
const listener = AppState.addEventListener('change', (state) => {
if (state === 'active') {
curStreakUpdate(dt);
// AsyncStorage.clear();
})
return () =>
listener.remove()
});
}, [dt]);

Timeout reset in React

I have a page with multiple forms on it. Anytime the form is submitted, I increase the value of actionsInARow (that is stored in context). What I want to do is, as soon as this value is >0, start a 2 second timer where we will re-fetch data from the database. But, anytime the user submits another form, I want to reset the timer. Below is the implementation I have so far, implemented on the component at the page level. Is there a better way to do this?
const [timerId, setTimerId] = React.useState(0);
useEffect(() => {
if(actionsInARow === 0) {
return;
}
if(timerId !== 0) {
window.clearTimeout(timerId)
}
let timeoutId
timeoutId = window.setTimeout(() => {
setTimerId(0)
// dispach event in context to reset the count to 0
dispatch({ type: "RESET_COUNT" });
// re-run fetch request here...
}, 2000)
setTimerId(timeoutId);
return () => {
if(timeoutId !== 0) {
window.clearTimeout(timeoutId)
}
}
}, [actionsInARow]);
Well for one thing you don't need the duplicate code to clear the timeout, the cleanup function will handle that for you so remove those 3 lines in the body. You also don't need the state of timerId, unless you're using that somewhere else.
Those changes would look like this
useEffect(() => {
if(actionsInARow === 0) {
return;
}
let timeoutId = window.setTimeout(() => {
// dispach event in context to reset the count to 0
dispatch({ type: "RESET_COUNT" });
// re-run fetch request here...
}, 2000)
return () => {
window.clearTimeout(timeoutId)
}
}, [actionsInARow]);

MutationObserver is reading old React State

I'm attempting to use a MutationObserver with the Zoom Web SDK to watch for changes in who the active speaker is. I declare a state variable using useState called participants which is meant to hold the information about each participant in the Zoom call.
My MutationObserver only seems to be reading the initial value of participants, leading me to believe the variable is bound/evaluated rather than read dynamically. Is there a way to use MutationObserver with React useState such that the MutationCallback reads state that is dynamically updating?
const [participants, setParticipants] = useState({});
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if(name in participants) {
// do something
} else {
setParticipants({
...participants,
[name] : initializeParticipant(name)
})
}
})
}
...
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, [speechObserverOn])
If you are dealing with stale state enclosures in callbacks then generally the solution is to use functional state updates so you are updating from the previous state and not what is closed over in any callback scope.
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participants) {
// do something
} else {
setParticipants(participants => ({
...participants, // <-- copy previous state
[name]: initializeParticipant(name)
}));
}
})
};
Also, ensure to include a dependency array for the useEffect hook unless you really want the effect to trigger upon each and every render cycle. I am guessing you don't want more than one MutationObserver at-a-time.
useEffect(() => {
if(!speechObserverOn) {
setSpeechObserverOn(true);
const speechObserver = new MutationObserver(onSpeechMutation);
const speechConfig = {
attributes: true,
attributeOldValue: true,
attributeFilter: ['class'],
subtree: true,
}
const participantsList = document.querySelector('.participants-selector');
if(participantsList) {
speechObserver.observe(participantsList, speechConfig);
}
}
}, []); // <-- empty dependency array to run once on component mount
Update
The issue is that if (name in participants) always returns false
because participants is stale
For this a good trick is to use a React ref to cache a copy of the current state value so any callbacks can access the state value via the ref.
Example:
const [participants, setParticipants] = useState([.....]);
const participantsRef = useRef(participants);
useEffect(() => {
participantsRef.current = participants;
}, [participants]);
...
const onSpeechMutation = (mutations) => {
mutations.forEach((mutation) => {
// identify name of speaker
if (name in participantsRef.current) {
// do something
} else {
setParticipants(participants => ({
...participants,
[name]: initializeParticipant(name)
}));
}
})
};

Using useState of complex object not working as expected in ReactJS

I have a function component and I am declaring a useState for a complex object like this:
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
I now try to set the state of the order by calling setOrder in a useEffect like this:
useEffect(() => {
(async function() {
let dh = new DataInterface("some string");
let errMsg = "";
// Get the sales order.
try
{
await dh.FetchOrder();
}
catch(error: any)
{
errMsg = error;
};
setOrder(salesOrder => ({...salesOrder, IsRetrieving: false, ErrorMsg: errMsg, DataInterface: dh}));
})();
}, []);
As is, this seems to work fine. However, I have a setInterval object that changes the screen message while order.IsRetrieving is true:
const [fetchCheckerCounter, setFetchCheckerCount] = useState<number>(0);
const statusFetcherWatcher = setInterval(() => {
if (order.IsRetrieving)
{
if (fetchCheckerCounter === 1)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "This can take a few seconds..."}));
}
else if (fetchCheckerCounter === 2)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "Almost there!.."}));
}
setFetchCheckerCount(fetchCheckerCounter + 1);
}
else
{
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
The issue is that order.IsRetrieving is always true for that code block, even though it does change to false, and my website changes to reflect that, even showing the data from dh.FetchOrder(). That means my timer goes on an infinite loop in the background.
So am I setting the state of order correctly? It's incredibly difficult to find a definite answer on the net, since all the answers are invariably about adding a new item to an array.
Issues
You are setting the interval as an unintentional side-effect in the function body.
You have closed over the initial order.isRetreiving state value in the interval callback.
Solution
Use a mounting useEffect to start the interval and use a React ref to cache the state value when it updates so the current value can be accessed in asynchronous callbacks.
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
const orderRef = useRef(order);
useEffect(() => {
orderRef.current = order;
}, [order]);
useEffect(() => {
const statusFetcherWatcher = setInterval(() => {
if (orderRef.current.IsRetrieving) {
if (fetchCheckerCounter === 1) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "This can take a few seconds...",
}));
} else if (fetchCheckerCounter === 2) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "Almost there!..",
}));
}
setFetchCheckerCount(counter => counter + 1);
} else {
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
return () => clearInterval(statusFetcherWatcher);
}, []);

React State is not updated with socket.io

When page loaded first time, I need to get all information, that is why I am calling a fetch request and set results into State [singleCall function doing that work]
Along with that, I am connecting websocket using socket.io and listening to two events (odds_insert_one_two, odds_update_one_two), When I got notify event, I have to
check with previous state and modify some content and update the state without calling again fetch request. But that previous state is still [] (Initial).
How to get that updated state?
Snipptes
const [leagues, setLeagues] = useState([]);
const singleCall = (page = 1, params=null) => {
let path = `${apiPath.getLeaguesMatches}`;
Helper.getData({path, page, params, session}).then(response => {
if(response) {
setLeagues(response.results);
} else {
toast("Something went wrong, please try again");
}
}).catch(err => {
console.log(err);
})
};
const updateData = (record) => {
for(const data of record) {
var {matchId, pivotType, rateOver, rateUnder, rateEqual} = data;
const old_leagues = [...leagues]; // [] becuase of initial state value, that is not updated
const obj_index = old_leagues.findIndex(x => x.match_id == matchId);
if(obj_index > -1) {
old_leagues[obj_index] = { ...old_leagues[obj_index], pivotType, rateOver: rateOver, rateUnder:rateUnder, rateEqual:rateEqual};
setLeagues(old_leagues);
}
}
}
useEffect(() => {
singleCall();
var socket = io.connect('http://localhost:3001', {transports: ['websocket']});
socket.on('connect', () => {
console.log('socket connected:', socket.connected);
});
socket.on('odds_insert_one_two', function (record) {
updateData(record);
});
socket.on('odds_update_one_two', function (record) {
updateData(record);
});
socket.emit('get_odds_one_two');
socket.on('disconnect', function () {
console.log('socket disconnected, reconnecting...');
socket.emit('get_odds_one_two');
});
return () => {
console.log('websocket unmounting!!!!!');
socket.off();
socket.disconnect();
};
}, []);
The useEffect hook is created with an empty dependency array so that it only gets called once, at the initialization stage. Therefore, if league state is updated, its value will never be visible in the updateData() func.
What you can do is assign the league value to a ref, and create a new hook, which will be updated each time.
const leaguesRef = React.useRef(leagues);
React.useEffect(() => {
leaguesRef.current = leagues;
});
Update leagues to leaguesRef.current instead.
const updateData = (record) => {
for(const data of record) {
var {matchId, pivotType, rateOver, rateUnder, rateEqual} = data;
const old_leagues = [...leaguesRef.current]; // [] becuase of initial state value, that is not updated
const obj_index = old_leagues.findIndex(x => x.match_id == matchId);
if(obj_index > -1) {
old_leagues[obj_index] = { ...old_leagues[obj_index], pivotType, rateOver:
rateOver, rateUnder:rateUnder, rateEqual:rateEqual};
setLeagues(old_leagues);
}
}
}

Resources