Electron/React cannot access component state changes in ipcRenderer listener - reactjs

I am using Electron with React and I am facing a little problem.
I am creating a functional component and in the useEffect hook I subscribe to ipcRenderer to listen for when ipcMain replies.
When ipcRenderer event triggers I am unable to access the latest state updates. All state variables values inside ipcRenderer.on function contain data when the component was initially created.
In the code below customers is an array state variable. If I console.log its value every time the ipcRenderer.on is fired it is always empty. I am absolutely sure this variable is not empty inside the component's context because it contains information that is rendered in a grid. When ipcRenderer.on is triggered my grid is reset or cleared. All I am trying to do is refresh a row in the grid when ipcRenderer.on is triggered.
useEffect(() => {
// Actions triggered in response to main process replies
ipcRenderer.on(IPCConstants.UPDATE_SALE_CUSTOMER, (event: any, arg: IUpdateResponse) => {
setIsLoading(false);
if(!arg.success) {
notifyError(arg.message);
return;
}
setCustomers(
customers.map(cst => {
if(cst.CUS_CustomerId === arg.customer.CUS_CustomerId){
cst.RowId = `${generateRandomValue()}`;
cst.isActive = arg.customer.isActive;
}
return cst;
})
)
});
return () => {
ipcRenderer.removeAllListeners(IPCConstants.UPDATE_SALE_CUSTOMER);
};
}, []);

useEffect(() => {
// Actions triggered in response to main process replies
ipcRenderer.on(IPCConstants.UPDATE_SALE_CUSTOMER, (event: any, arg: IUpdateResponse) => {
setIsLoading(false);
if(!arg.success) {
notifyError(arg.message);
return;
}
setCustomers(
customers.map(cst => {
if(cst.CUS_CustomerId === arg.customer.CUS_CustomerId){
cst.RowId = `${generateRandomValue()}`;
cst.isActive = arg.customer.isActive;
}
return cst;
})
)
});
return () => {
ipcRenderer.removeAllListeners(IPCConstants.UPDATE_SALE_CUSTOMER);
};
}, [customers, otherState (if needed)]); // <= whichever state is not up to date
You need to include the proper dependencies.
the useEffect will keep up to date with these state values
alternatively if you don't need the functionality of useState (unlikely) you can use useRef and the useEffect will always have the up to date ref without passing it as a dependency.

Related

Updated state unavailable when accessing inside a method getting called from useEffect [React]

In a functional React component, I'm trying to do a fairly simple operation-
const TestClass = ({lastIndex}) => {
const [activeIndex, setActiveIndex] = useState(0);
const [fullScreenGallery, setFullScreenGallery] = useState(false);
const handleKeyChange = (e) => {
switch(e.code) {
case 'ArrowLeft':
if(activeIndex - 1 < 0) return setActiveIndex(lastIndex);
return setActiveIndex(activeIndex-1);
case 'ArrowRight':
if(activeIndex + 1 > lastIndex) return setActiveIndex(0);
return setActiveIndex(activeIndex+1);
case 'Escape':
if(fullScreenGallery) {
// closeFullScreenGallery();
return;
}
}
}
useEffect(() => {
document.addEventListener('keydown', handleKeyChange);
// other event listeners
}, []);
return (
<div>
// some code
</div>
)
}
Issue is, inside handleKeyChange, the value of activeSlide doesn't change after 1, even when I press the key. Can anyone point-out what I'm doing wrong? Same goes for fullScreenGallery.
If I try to add activeSlide and fullScreenGallery as dependencies in useEffect, my event listener starts getting adding whenever the dependency changes and I end up crashing my page.
This code was part of a class component and I'm trying to convert this class to a functional component. In the class component, useEffect code was written inside componentDidMount.
You need to "watch" for activeIndex changes.
by setting [] as you did, your useEffect is only launched once. Therefore your activeIndex varaible which is used inside this function always contains the initial value.
Learn more here.
https://fr.reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
useEffect(() => {
document.addEventListener('keydown', handleKeyChange);
// other event listeners
}, [activeIndex]);
you also need to removelisteners now. or you will rapidly become overwhelmed by the increasing number of events.
useEffect(() => {
document.addEventListener('keydown', handleKeyChange);
return () => {
document.removeEventListener('keydown', handleKeyChange);
}
// other event listeners
}, [activeIndex]);
I'm still not sure if hooks are better than classes.

does setInterval inside a useEffect can recognize an updated state?

I'm working on an app which requires a user location every X seconds. When the component is mounted, the interval starts and fetch the GPS location and compare it to the old one which is saved on the component state.
The thing is, the interval is comparing only to the default state of the variable, so if the new value is different than the old one, a setter is called to change that value.
I used useEffect on the location var so whenever it changes, to just print it and it does correctly by the new value.
but the interval keeps using the default value given in useState hook.
What is it that I'm missing here.
An example of my code is below:
const [currentLocation, setCurrentLocation] = useState(null);
let locationInterval = null;
useEffect(() => {
locationInterval = setInterval(async () => {
console.log("IN INTERVAL");
navigator.geolocation.getCurrentPosition((location, error) => {
if (location) {
location = [location.coords.latitude, location.coords.longitude];
/// currentLocation is not updating here for some reason
if (JSON.stringify(location) !== JSON.stringify(currentLocation)) {
alert(
`the new location in interval ${location} old location: ${currentLocation}`
);
setCurrentLocation(location);
}
} else {
console.log(error);
}
});
}, 15000);
}, [map]);
useEffect(() => {
return () => {
clearInterval(locationInterval);
};
}, []);
useEffect(() => {
/// currentLocation is updated here from the setInterval
console.log("newlocation", currentLocation);
}, [currentLocation]);
The reason is because currentLocation value is not given as a dependency in the useEffect, therefore that useEffect callback function only has access to the original useState value.
May I ask are there any React warning on your terminal where you run your react app?
Something like
React Hook useEffect has a missing dependency: 'currentLocation'
But on the side node, if you add currentLocation to the dependency,since because you are updating currentLocation in your useEffect and if it's updated, it will re-run the useEffect callback again.
You can not get an updated state in setInterval because callback inside useEffect hook only called when its dependency array updates([map]).
When it is called currentLocation is passed as a parameter to setInterval's callback. Which is the default value for useState at that time.
To prevent this you can do this.
const [currentLocationRef] = useRef(null);
useEffect(() => {
locationInterval = setInterval(async () => {
//....
if (JSON.stringify(location) !== JSON.stringify(currentLocationRef.current)) {
currentLocationRef.current = location;
}
}, 15000);
return () => {
clearInterval(locationInterval);
};
}, [map, currentLocationRef]);
Also, you should return clearInterval in the same useEffect Callback.
The reason behind this is currentLocationRef.current is different from currentLocation. The first one is the getter function and the second one is value. That's why the first one is still able to access updated value while the second one cannot.

React functional component access state in useeffect

I've gote some react component like below. I can use "messages" in return, but if I try to access messages inside some function, or useEffect, as in example, I always become initial value. How can I solve it in functional component? Thanks
const Messages = () => {
const { websocket } = useContext(WebsocketsContext);
let [ messages, setMessages ] = useState([]);
useEffect(() => {
getMessages()
.then(result => {
setMessages(result);
})
}, []);
useEffect(() => {
if(websocket != null){
websocket.onmessage = (msg) => {
let wsData = JSON.parse(msg.data);
if(wsData.message_type == 'Refresh'){
console.log(messages)
};
};
};
}, [websocket]);
return(
<div>...</div>
);
};
export default Messages;
Looks like you have encountered a stale closure
the useEffect with [websockets] in its dependency array will only ever "update" whenever the websocket reference/value changes. Whenever it does, the function will have created a "closure" around messages at that point in time. Thus, the value of messages will stay as is within that closure. If messages updates after websocket has been created, it will never update the value of "messages" within the onmessage callback function.
To fix this, add "messages" to the dependency array. [websockets, messages]. This will ensure the useEffect callback always has the latest state of messages, and this the onmessage function will have the latest state of messages.
useEffect(() => {
if(websocket != null){
websocket.onmessage = (msg) => {
let wsData = JSON.parse(msg.data);
if(wsData.message_type == 'Refresh'){
console.log(messages)
};
};
};
}, [websocket, messages]);
It's because your getMessages() is an async function. The order is as follows: component mounts initially and values are initialized -> componentDidMount() is invoked meaning your getMessages() is invoked (an async function!) -> your webaocket is initialized and invokes the second useEffect, which reads the initial value of messages -> your getMessages gets its response and sets the messages accordingly.
To make it work as intended, make the second useEffect's dependency array as [websocket, messages].

In React, how to call function based on const value change

I have 3 components in react. One of them is the parent component and the remaining two are the child components (EziSchedule & EziTransaction), where each component is getting its own data by calling the API. however data to show in child component EziTransaction depends upon what record I click on table in schedule class.
when user click on record in EziSchedule, it goes back to parent component followed by EziTransaction and can print correct id in EziTransaction.
I need to refresh data in EziTransaction component which I am unable to do so. I believe I need to use state changed in order to call getEziTransactionData in EziTransaction component to refresh data but not sure how to do it.
Parent component
const EziTrackerParrent = () =>{
const [data, setData] = useState('schedule');
useEffect(() =>{
},[]);
return (
<div>
<h3>EziSchedule Table</h3>
<EziSchedule change={setData} ></EziSchedule>
<h3>EziTransaction Table</h3>
<EziTransaction data={data}></EziTransaction>
</div>
)
Child Component A - EziSchedule
Capture click event and pass it to parent
const EziSchedule = ({change}) =>{
Child Component B - EziTransaction
Get data from EziSchidule on click event via parent component. I need help here to ensure very time 'data' value changes, It call getEziTransactionData() and refresh html
const EziTransaction = ({data}) =>{
const [eziTransactionData, setEziTransactionData] = useState<IEziTransaction[]>();
useEffect(() => {
getEziTransactionData(); // this method call API to get data
},[]);
const getEziTransactionData = ()=>{ // I need to call this everytime user click on record in EziSchedule???
(async () =>{
try{
const result = //API Call...
setEziTransactionData(result);
........
return(
<div>
<div>I have received "{data}" from parent</div> // this values does change every click in EziSchedule
to trigger getEziTransactionData everytime dataupdates you need to pass data as dependency at useEffect
useEffect(() => {
getEziTransactionData();
},[data]);
You can run the useEffect hook on props/state change by adding that variable in dependency array if the useEffect hook.
In your case you can add data to dependency of useEffect and useEffect will run every time the variable data changes.
If you are using data in your method to make api call, you can make that function a callback and just add that function to the dependency of useEffect, so every time data will change, your callback function will change, which will trigger the useEffect hook.
const EziTransaction = ({data}) => {
const [eziTransactionData, setEziTransactionData] = useState<IEziTransaction[]>();
// wrap this function in a useCallback hook
const getEziTransactionData = useCallback(async () => {
try {
const result = await fetchApiData(data) // assuming you are using `data` in your API Call, if not add it to useEffect dependency and remove from this callback's dependency
setEziTransactionData(result);
} catch (err) {
console.log(err)
}
}, [setEziTransactionData, data]); // set dependency array of callback properly
useEffect(() => {
getEziTransactionData();
},[getEziTransactionData]); // set dependency array properly
return(
<div>
<div>I have received "{data}" from parent</div>
</div>
);
}
in addition to the answer below i would recomend to define you fetch function inside useEffect with data dependency to avoid extra memoization
useEffect(() => {
const getEziTransactionData = async () => {
try {
const result = await fetchApiData(data);
setEziTransactionData(result);
} catch (err) {
console.error(err);
}
});
getEziTransactionData();
}, [data]);

how to use the useEffect hook on component unmount to conditionally run code

For some odd reason the value of props in my "unmount" useEffect hook is always at the original state (true), I can console and see in the devtools that it has changed to false but when the useEffect is called on unmount it is always true.
I have tried adding the props to the dependancies but then it is no longer called only on unmount and does not serve it's purpose.
Edit: I am aware the dependancy array is empty, I cannot have it triggered on each change, it needs to be triggered ONLY on unmount with the update values from the props. Is this possible?
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, []);
How can I conditionally run my code on unmount with the condition being dependant on the updated props state?
If you want code to run on unmount only, you need to use the empty dependency array. If you also require data from the closure that may change in between when the component first rendered and when it last rendered, you'll need to use a ref to make that data available when the unmount happens. For example:
const onUnmount = React.useRef();
onUnmount.current = () => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
}
React.useEffect(() => {
return () => onUnmount.current();
}, []);
If you do this often, you may want to extract it into a custom hook:
export const useUnmount = (fn): => {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffect(() => () => fnRef.current(), []);
};
// used like:
useUnmount(() => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
});
The dependency list of your effect is empty which means that react will only create the closure over your outer variables once on mount and the function will only see the values as they have been on mount. To re-create the closure when report.data.draft changes you have to add it to the dependency list:
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, [report.data.draft]);
There also is an eslint plugin that warns you about missing dependencies: https://www.npmjs.com/package/eslint-plugin-react-hooks
Using custom js events you can emulate unmounting a componentWillUnmount even when having dependency. Here is how I did it.
Problem:
useEffect(() => {
//Dependent Code
return () => {
// Desired to perform action on unmount only 'componentWillUnmount'
// But it does not
if(somethingChanged){
// Perform an Action only if something changed
}
}
},[somethingChanged]);
Solution:
// Rewrite this code to arrange emulate this behaviour
// Decoupling using events
useEffect( () => {
return () => {
// Executed only when component unmounts,
let e = new Event("componentUnmount");
document.dispatchEvent(e);
}
}, []);
useEffect( () => {
function doOnUnmount(){
if(somethingChanged){
// Perform an Action only if something changed
}
}
document.addEventListener("componentUnmount",doOnUnmount);
return () => {
// This is done whenever value of somethingChanged changes
document.removeEventListener("componentUnmount",doOnUnmount);
}
}, [somethingChanged])
Caveats: useEffects have to be in order, useEffect with no dependency have to be written before, this is to avoid the event being called after its removed.

Resources