Im new to react and Im not sure why do we need a cleanup function when dealing with EventListeners, Iam trying to set an event when resizing my window but i only have 1 event in (Elements -> Event Listeners) tab in chrome dev tools, even if I don't return a cleanup function in my hook
Heres my code:
useEffect(() => {
window.addEventListener("resize", function checksize() {
console.log("1");
});
});
First of all, you should absolutely avoid using that in your code, because on each rerender, it is going to add a new event listener to the window.
Secondly, to answer your question, you should have a cleanup effect to remove event listeners and other similar mechanisms to avoid memory leaks. If you don't clean them up, then you leave dangling eventlisteners taking up memory which is not a good idea, and may be picked up on within your other components as well. So the ideal way to handle this kind of code is
const logOne = () => console.log("1"); //put the function reference in a variable so that you can remove the event afterwards
useEffect(() => {
window.addEventListener("resize", logOne);
return () => {
window.removeEventListener("resize", logOne); //remove event listener on unmount
}
}, []); //empty dependency array so that function only runs on first render, so that consequent rerenders don't cause you to add more of these event listeners
Because you will keep adding listeners every render. Instead of printing "1" once, it will print it twice next time. Then 3 times on the next re-render and so on.
Related
There is a web application - a simple widget that loads local time from the WorldTime API for different regions.
The task is to fetch time from the server every five seconds.
To do this, I used setInterval inside useEffect , in which I put the function where fetch occurs. However, there is a problem - when selecting / changing the region, I have to wait five seconds until setInterval completes.
setInterval, as I understand it, is restarted inside useEffect when deps changes, when a new region is selected.
How can this problem be solved?
App on github: https://mmoresun.github.io/live-clock/
The code itself: https://codesandbox.io/s/codepen-with-react-forked-66iz3n?file=/src/App.js
You can call directly the same getTime function before setting the interval
useEffect(() => {
getTime(value);
const myInterval = setInterval(() => {
// refreshing with setInterval every 5 seconds
getTime(value);
}, 5000);
return () => clearInterval(myInterval);
}, [value]); /
As for your codepen example, you are continuosly calling the timezone api, because you fetch it inside the SearchPanel component. That's wrong, remove it.
I am using react app context to store an array of "alerts objects" which is basically any errors that might occur and I would want to show in the top right corner of the website. The issue I am having is that the context is not being up to date inside a timeout. What I have done for testing is gotten a button to add an alert object to the context when clicked and another component maps through that array in the context and renders them. I want them to disappear after 5 seconds so I have added a timeout which filters the item that got just added and removes it. The issue is that inside the timeout the context.alerts array seems to have the same value as 5 seconds ago instead of using the latest value leading to issues and elements not being filtered out. I am not sure if there's something wrong with my logic here or am I using the context for the wrong thing?
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts([
...context.alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
onClick={() => {
const errorPopup = getPopup(); // Get's the alert object I need
context.setAlerts([errorPopup, ...context.alerts]);
setTimeout(() => {
context.setAlerts(alerts => [
...alerts.filter(
(element) => element.id !== errorPopup.id,
),
]);
}, 5000);
}}
This should fix it. Until react#17 the setStates in an event handler are batched ( in react#18 all setStates are batched even the async ones ), hence you need to use the most fresh state to make the update in second setAlerts.
To be safe it's a good practice using the cb syntax in the first setState as well.
I think the fix would be to move context.setAlerts(...) to a separate function (say removePopupFromContext(id:string)) and then call this function inside the setTimeout by passing the errorPopup.Id as parameter.
I'm not sure of your implementation of context.setAlerts, but if it's based on just setState function, then alternatively, you could do also something similar to how React let's you access prevState in setState using a function which will let you skip the creation of the extra function which may lightly translate to:
setContext(prevContextState =>({
...prevContextState,
alerts: prevContextState.alerts.filter(your condition)
)})
This is my state:
const [markers, setMarkers] = useState([])
I initialise a Leaflet map in a useEffect hook. It has a click eventHandler.
useEffect(() => {
map.current = Leaflet.map('mapid').setView([46.378333, 13.836667], 12)
.
.
.
map.current.on('click', onMapClick)
}, []
Inside that onMapClick I create a marker on the map and add it to the state:
const onMapClick = useCallback((event) => {
console.log('onMapClick markers', markers)
const marker = Leaflet.marker(event.latlng, {
draggable: true,
icon: Leaflet.divIcon({
html: markers.length + 1,
className: 'marker-text',
}),
}).addTo(map.current).on('move', onMarkerMove)
setMarkers((existingMarkers) => [ ...existingMarkers, marker])
}, [markers, onMarkerMove])
But I would also like to access the markers state here. But I can't read markers here. It's always the initial state. I tried to call onMapClick via a onClick handler of a button. There I can read markers. Why can't I read markers if the original event starts at the map? How can I read the state variables inside onMapClick?
Here is an example: https://codesandbox.io/s/jolly-mendel-r58zp?file=/src/map4.js
When you click in the map and have a look at the console you see that the markers array in onMapClick stays empty while it gets filled in the useEffect that listens for markers.
React state is asynchronous and it won't immediately guarantee you to give you the new state, as for your question Why can't I read markers if the original event starts at the map its an asynchronous nature and the fact that state values are used by functions based on their current closures and state updates will reflect in the next re-render by which the existing closures are not affected but new ones are created, this problem you wont face on class components as you have this instance in it, which has global scope.
As a developing a component , we should make sure the components are controlled from where you are invoking it, instead of function closures dealing with state , it will re-render every time state changes . Your solution is viable you should pass a value whatever event or action you pass to a function, when its required.
Edit:- its Simple just pass params or deps to useEffect and wrap your callback inside, for your case it would be
useEffect(() => {
map.current = Leaflet.map('mapid').setView([46.378333, 13.836667], 12)
.
.
.
map.current.on('click',()=> onMapClick(markers)) //pass latest change
}, [markers] // when your state changes it will call this again
for more info check this one out https://dmitripavlutin.com/react-hooks-stale-closures/ , it will help you for longer term !!!
Long one but you'll understand why this is happening and the better fixes. Closures are especially an issue (also hard to understand), mostly when we set click handlers which are dependent on the state, if the handler function with the new scope is not re-attached to the click event, then closures remain un-updated and hence the stale state remains in the click handler function.
If you understand it perfectly in your component, useCallback is returning a new reference to the updated function i.e onMapClick having your updated markers ( the state) in its scope, but since you are setting the 'click' handler only in the beginning when the component is mounted, the click handler remains un-updated since you've put a check if(! map.current), which prevents any new handler to be attached on the map.
// in sandbox map.js line 40
useEffect(() => {
// this is the issue, only true when component is initialized
if (! map.current) {
map.current = Leaflet.map("mapid4").setView([46.378333, 13.836667], 12);
Leaflet.tileLayer({ ....}).addTo(map.current);
// we must update this since onMapClick was updated
// but you're preventing this from happening using the if statement
map.current.on("click", onMapClick);
}
}, [onMapClick]);
Now I tried moving map.current.on("click", onMapClick); out of the if block, but there's an issue, Leaflets instead of replacing the click handler with the new function, it adds another event handler ( basically stacking event handlers ), so we must remove the old one before adding the new one, otherwise we will end up adding multiple handlers each time onMapClick is updated. For which we have the off() function.
Here's the updated code
// in sandbox map.js line 40
useEffect(() => {
// this is the issue, only true when component is initialized
if (!map.current) {
map.current = Leaflet.map("mapid4").setView([46.378333, 13.836667], 12);
Leaflet.tileLayer({ ....
}).addTo(map.current);
}
// remove out of the condition block
// remove any stale click handlers and add the updated onMapClick handler
map.current.off('click').on("click", onMapClick);
}, [onMapClick]);
This is the link to the updated sandbox which is working just fine.
Now there's another Idea to solve it without replacing click handler each time. i.e some globals, which I believe is not really too bad.
For this add globalMarkers outside but above your component and update it each time.
let updatedMarkers = [];
const Map4 = () => {
let map = useRef(null);
let path = useRef({});
updatedMarkers = markers; // update this variable each and every time with the new markers value
......
const onMapClick = useCallback((event) => {
console.log('onMapClick markers', markers)
const marker = Leaflet.marker(event.latlng, {
draggable: true,
icon: Leaflet.divIcon({
// use updatedMarkers here
html: updatedMarkers.length + 1,
className: 'marker-text',
}),
}).addTo(map.current).on('move', onMarkerMove)
setMarkers((existingMarkers) => [ ...existingMarkers, marker])
}, [markers, onMarkerMove])
.....
} // component end
And this one works perfectly too, Link to the sandbox with this code. This one works faster.
And lastly, the above solution of passing it as a param is okay too! I prefer the one with updated if block since it's easy to modify and you get the logic behind it.
Currently, I am checking if the input useEffect() hook dependency is at least a certain length before calling loadSearchData() which is an async method that hits an API.
useEffect(() => {
if (input.length >= MIN_CHAR_INPUT) {
loadSearchData();
}
}, [input]);
Is there a way where I could move the input check to the input dependency param for useEffect()? Probably a case where I need to write a custom hook.
Is there a way where I could move the input check to the input dependency param for useEffect()? Probably a case where I need to write a custom hook.
I'd build it this way:
function useEffect2(effect, deps, isValid = true) {
const cleanup = React.useRef(null);
useEffect(() => {
if (isValid) {
if (typeof cleanup.current === "function") {
// schedule cancellation of previous request right after effect has been called
// using the Promise construct here so I don't have to deal with a throwing cancellation function
Promise.resolve().then(cleanup.current);
}
// in case effect() throws,
// don't want to call the old cancellation function twice
cleanup.current = null;
// get new cancel-function
cleanup.current = effect();
}
}, deps);
useEffect(() => () => {
// deal with cancellation on unmount
typeof cleanup.current === "function" && cleanup.current();
}, []);
}
useEffect2(loadSearchData, [input], input.length >= MIN_CHAR_INPUT);
I just want to clarify the cancel. This will give us access to the current useEffect() call in the stack and allow us to properly handle the call without any memory-leaks
From https://reactjs.org/docs/hooks-effect.html#recap
We’ve learned that useEffect lets us express different kinds of side effects after a component renders. Some effects might require cleanup so they return a function
Cleanup is probably a better name. I use it the most to "cancel" previous ajax-requests if they are still pending/prevent them to update the state. I've renamed the variable in the code.
What we're trying to emulate here is a useEffect that runs the effect conditionally. So when the condition is false, we don't want the effect to cleanup the previous call; as if the deps didn't change. Therefore we need to handle the cleanup function ourselves, and when/wether it should be invoked. That's
when (and only if) we call the effect function
on componentWillUnmount
That's what this ref is for. Since the reference is overwritten with every call to effect this shouldn't leak any memory.
I have encountered strange behavior when using Electron's ipcRenderer with React's useEffect.
Within my electron app, I have the following code:
import React, { useEffect } from 'react'
const electron = window.require('electron');
const ipcRenderer = electron.ipcRenderer;
...
const someValueThatChanges = props.someValue;
useEffect(() => {
const myEventName = 'some-event-name';
console.log(`Using effect. There are currently ${ipcRenderer.listenerCount(eventName)} listeners.`);
console.log(`Value that has changed: ${someValueThatChanges}.`);
ipcRenderer.addListener(myEventName, myEventHandler);
console.log('Added a new listener.');
// Should clean up the effect (remove the listener) when the effect is called again.
return () => {
ipcRenderer.removeListener(myEventName, myEventHandler)
console.log('Cleaned up event handler.');
}
}, [ someValueThatChanges ]);
function myEventHandler() {
console.log('Handled event');
}
The code above is supposed to listen to the some-event-name event fired by Electron's main process with mainWindow.webContents.send('some-event-name'); and console.log(...) a message inicating that the event was handled.
This works as expected when the effect is initially run. A listener is added, the event is raised at a later time, and the string 'Handled event' is printed to to the console. But when the someValueThatChanges variable is assigned a different value and the event is raised for a second time, the 'Handled event' string is printed out to the console twice (the old listener does not appear to have been removed).
The line with the listenerCount(eventName) call returns 0 as expected when the removeListener(...) call is included in the useEffect return/cleanup function. When the removeListener(...) call is removed, the listenerCount(eventName) call returns a value that is incremented as expected (e.g. 0, 1, 2) as listeners are not removed.
Here's the really weird part. In either case, whether or not I include the call to removeListener(...), the myEventHandler function is always called for as many times as useEffect has been run. In other words, Electron reports that there are no event listeners, but myEventHandler still seems to be called by the previous listeners. Is this a bug in Electron, or am I missing something?
Never try with ipcRenderer.addListener, But try ipcRenderer.on instead
useEffect(() => {
ipcRenderer.send('send-command', 'ping');
ipcRenderer.on('get-command', (event, data) => {
console.log('data', data);
});
return () => {
ipcRenderer.removeAllListeners('get-command');
};
}, []);
I believe, the docs changed. ipcRenderer.removeAllListeners accept single string instead of array of string Source electron issues,