useEffect run once hook - javascript

I found this lib https://react-hooks.org/docs/useEffectOnceWhen but I'm confused. Why does it even existed and in what purpose it's used? Isn't I can achieve the same thing using useEffect?
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 3000); // Countdown for 3 sec
}, [])

The hook is useEffectOnceWhen, not just useEffectOnce. Per the source you linked:
Runs a callback effect atmost one time when a condition becomes true
Here's the source code of that hook:
/**
* useEffectOnceWhen hook
*
* It fires a callback once when a condition is true or become true.
* Fires the callback at most one time.
*
* #param callback The callback to fire
* #param when The condition which needs to be true
*/
function useEffectOnceWhen(callback: () => void, when: boolean = true): void {
const hasRunOnceRef = useRef(false);
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
useEffect(() => {
if (when && !hasRunOnceRef.current) {
callbackRef.current();
hasRunOnceRef.current = true;
}
}, [when]);
}
As you can see, it does something more advanced than just being called once at the beginning of the hook: it gets called once the condition passed in becomes true for the first time.

You can do the same thing with useEffect:
"If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument"
Docs
You can also supply a list of dependencies that trigger the effect running again if they change.
useEffect( () => { doSomething() } , [ifThisChangesRunFuncAgain, orThis, ...] )

Related

how to prevent stacking function callbacks while exiting a screen react/react-native

I have a local state selectedProducts holding a number representing the number of products the client has selected, while exiting the screen I want to update the number in redux so it can be viewed in the cart on a different screen.
but every time my local selectedProducts updates it stacks another function call on beforeRemove event
I know this is a noob problem but I've spent hours trying to find a solution to this problem and advice would be very helpful :)
useEffect(()=>{
navigation.addListener('beforeRemove', () => {
console.log('check this', selectedProducts);
if (!selectedProducts) {
return;
}
dispatch(addToCartMultipleQty(selectedProducts));
});
},[selectedProducts])
selectedProducts is a number state whose initial value is null and on a button click event its value is either incremented or decremented by 1, if its previous value is null then its value is set to 1
Note: I just want to update selectedProducts state's latest value only once in redux when the screen is about to be exited/unmonted
You can try this:
useEffect(()=>{
navigation.addListener('beforeRemove', () => {
console.log('check this', selectedProducts);
if (!selectedProducts) {
return;
}
dispatch(addToCartMultipleQty(selectedProducts));
});
return () => {
navigation.removeListener('beforeRemove');
}
}, [selectedProducts])
Add that in return at the end of useEffect it will work as componentWillUnmount in functional component
useEffect(() => {
return () => {
// Anything in here is fired on component unmount.
}
}, [])
Edit: In Your case
useEffect(() => {
console.log('check this', selectedProducts);
if (!selectedProducts) {
return;
}
return () => {
// Anything in here is fired on component unmount.
if (selectedProducts) {
dispatch(addToCartMultipleQty(selectedProducts));
}
};
}, [selectedProducts]);

Why do I see stale data even after invalidating my queries?

I have created a function which adds a specific item to my diary. 9/10 times everything works, which means that there is nothing wrong with the code?
However rarely I add the item to my diary, but I don't see the update values, even thought I activated queryClient.invalidateQueries() method, the value is updated on my server, because when I manually refresh I see the updated diary again.
Does this mean that by the time I activate invalidatequeries method, the update has not reached my server and that is why I am seeing stale data? But what would I do in that case?
Here is the function:
const newAddItemFunction = () => {
const day = newDiary?.[currentDay];
if (day && selectedMealNumber && selectedItem) {
setSavingItem(true);
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
router.push("/diary");
}
};
Here is my custom hook(useFirestoreQuery is just custom wrapped useQuery hook for firebase):
export const useGetCollectionDiary = () => {
const user = useAuthUser(["user"], auth);
const ref = collection(
firestore,
"currentDiary",
user.data?.uid ?? "_STUB_",
"days"
);
return useFirestoreQuery(
["currentDiary"],
ref,
{
subscribe: false,
},
{
select: (data) => {
let fullDaysArray = [] as Day[];
data.docs.map((docSnapshot) => {
const { id } = docSnapshot;
let data = docSnapshot.data() as Day;
data.documentId = id;
fullDaysArray.push(data);
});
fullDaysArray.sort((a, b) => a.order - b.order);
return fullDaysArray;
},
enabled: !!user.data?.uid,
}
);
};
NewAddItemToDiary function is just firebase call to set document:
//...json calculations
setDoc(
doc(
firestore,
"currentDiary",
auth.currentUser.uid,
"days",
day.documentId
),
newDiaryWithAddedItem
);
9/10 times everything works, which means that there is nothing wrong with the code?
It indicates to me that there is something wrong with the code that only manifests in edge cases like race conditions.
You haven't shared the code of what NewAddItemToDiary is doing, but I assume it's asynchronous code that fires off a mutation. If that is the case, it looks like you fire off the mutation, and then invalidate the query without waiting for the query to finish:
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
Mutations in react-query have callbacks like onSuccess or onSettled where you should be doing the invalidation, or, if you use mutateAsync, you can await the mutation and then invalidate. This is how all the examples in the docs are doing it:
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})

state update from a callback

The following member function populates asynchronously a folder_structure object with fake data:
fake(folders_: number, progress_callback_: (progress_: number) => void = (progress_: number) => null): Promise<boolean>
{
return new Promise((resolve, reject) => {
for (let i_ = 0; i_ < folders_; i_++) {
progress_callback_(i_ / folders_ * 100.);
this.add(this.id(), faker.address.city() + i_, random_choice(this.folder_structure_id()));
}
progress_callback_(folders_ / folders_ * 100.);
resolve(true);
})
}
It uses a callback to update the progress within the for loop which is then used to update the state (a progress bar) from within a useEffect() function with an empty dependency array.
let [progress_state_, set_progress_state_] = useState<number>(0);
let [fake_done_, set_fake_done_] = useState<boolean>(false);
useEffect(() =>
{
if (fake_)
folder_structure_.fake(fake_, (progress_) => {
set_progress_state_(progress_)
}).then(value => set_fake_done_(value));
}, [])
if (!fake_ || fake_done_) etc etc
However, the state is not updated (logging the progress in the console seems to work fine). Any ideas as to whether it's possible to update a state from within useEffect?
The reason your useEffect hook isn't working is that it's not called upon progress_state_ state change.
Instead of
useEffect(() =>
{
...
}, [])
Try this instead
useEffect(() =>
{
...
}, [progress_])
Adding progress_ to the dependency array means useEffect will be called every single time progress_ changes. If you leave it as an empty dependency array, then useEffect is only ever called in the very beginning on when the code is mounted to the DOM.
Here's a good explanation on dependency arrays: https://devtrium.com/posts/dependency-arrays
Addressing your final question: Yes, it is possible to update state from within useEffect.
To understand the root of your main issue, I would be curious to see how you are doing your logging. Are you logging from within fake() or from your render() function?

Need to execute async function for every element of the array but dispatch action with progress only

I am quite new in RxJS, still trying to figure out how to implement different features using it.
I need help regarding the implementation of an observable, tried so many ways but none seems to work.
I have this function:
export function automateParameterEdit(tunId) {
const progress$ = new Subject();
const process$ = defer(async () => {
const tun = await updateStatus(tunId, 'autoTun');
progress$.next({ ...tun , progress: '0' });
return { rules: tun.rules, tun };
}).pipe(
flatMap(({ rules, tun }) =>
from(Object.values(rules)).pipe(
concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId)),
scan((acc, curr) => acc + 1, 0),
map(progress => {
progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 });
}),
catchError(e => {
// whatever
}),
finalize(async () => {
// whatever
})
)
)
);
return merge(progress$, process$);
}
So, right now, the action is being dispatched twice, once because of the progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 }); that emits the new tun progress, and the second time I believe it's because of the execution of: concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId))
Let's say there are 4 rules (Object.values(rules).length === 4). In the console I see 4 x 2 = 8 actions dispatched, half of them have invalid payload.
What I want to do is execute the autoEditParameters(tunId, rule.ruleId, tun.rulesetId) which btw is async, and after each execution I want to emit the progress (progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 });).
How do I stop the invalid actions from being dispatched and only execute the async autoEditParameters and dispatch progress?
You don't need a Subject!
You only need a subject when you need to "manually" push a value through a stream. But, in your case, you just want to modify (map) emissions into a different shape.
So, you can just get rid of the subject. There's no need to merge process$ with progress$; you can simply return progress$;
function automateParameterEdit(tunId) {
const process$ = defer(async () => {
const tun = await updateStatus(tunId, 'autoTun');
return { rules: tun.rules, tun };
}).pipe(
flatMap(({ rules, tun }) =>
from(Object.values(rules)).pipe(
concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId)),
scan((acc, curr) => acc + 1, 0),
map(progress => {
return { ...tun, progress: progress / Object.values(rules).length * 100 };
})
)
)
);
return process$;
}
Here are a couple StackBlitz samples:
Original
Possible Solution
and after each execution I want to emit the progress
Not sure if you meant you just wanted emit the numeric percent (not an object), but that could easily be done. Sometimes breaking it down into smaller functions can make it easier to follow:
function automateParameterEdit(tunId): Observable<number> {
return updateTun(tunId).pipe(
flatMap(processRules)
);
}
function updateTun(tunId): Observable<Tun> {
return defer(async () => updateStatus(tunId, 'autoTun'))
}
function processRules(tun: Tun): Observable<number> {
return from(tun.rules).pipe(
concatMap(rule => autoEditParameters(tun.id, rule.ruleId, tun.rulesetId)),
scan(acc => acc + 1, 0),
map(doneCount => doneCount / tun.rules.length * 100),
startWith(0),
)
}
Here, updateTun() just wraps the async function and returns an observable, so it will be executed whenever it is subscribed to.
processRules() takes a Tun and returns an Observable<number> that is the progress percent. startWith just emits an intial value of 0.
StackBlitz

Default value for vuex action payload?

I'm trying to come up with a nicer way to have a default value for a vuex action payload property, right now I'm using an if-else to check if the passed payload object has a delay property, if it doesn't I set the value to default and you can imagine the rest.
Is there a nicer way to do this in fewer lines? I'm sure there must be.
Here is my action:
showModal ( {commit}, modalPayload ) {
let delay;
if(modalPayload.delay == undefined){
delay = 3000;
}
else{
delay = modalPayload.delay
}
commit('SHOW_MODAL', modalPayload);
setTimeout(function(){
commit('HIDE_MODAL');
}, delay);
},
Thanks in advance.
You could set a default value using destructuring assignment:
showModal ({ commit }, modalPayload) {
const { delay = 3000 } = modalPayload
commit('SHOW_MODAL', modalPayload);
setTimeout(() => commit('HIDE_MODAL'), delay);
}
Also, if you don't need passing delay to the commit, you can destructure the second function parameter:
showModal ({ commit }, { delay = 3000, ...modalPayload }) {
commit('SHOW_MODAL', modalPayload);
setTimeout(() => commit('HIDE_MODAL'), delay);
}

Categories