Pass state data to ipc function call - javascript

Problem
The event to send data to renderer is triggered on a file change with chokidar. When the file watcher is triggered and the process sends the event to the renderer.
My problem is that when the EmitterEvent is triggered I input the current state of my useState() variable but only the initial state is passed to my function.
Edited: my problem is that I can not pass the updated data variable to the updateData(newData) function which is called from the emitter inside the preload.js.
Question
How can I pass the state variable data to the call ?
Is there a way that I can change my preload.js in order for the api.receive function to return a string in order not to have to pass a function to the emitter ? (please check the updateData(newData) function for more info)
Is there a better way to achieve this ?
This could also help me to initialize the data for the first render.
preload.js
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
// whitelist channels
let validChannels = ["file", "save"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["file", "save"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
}
electron.js
function ReadNodesFileToIpc(path) {
fs.readFile(path, (error, data) => {
win.webContents.send("file", data);
});
}
Code in component that receives data
function MyForceGraphComponent(){
const [data, setData] = useState({ nodes: [{ id: 0, otherinfo: [] }], links: [] });
var isDataInittiallized = 0;
...
function updateData (newData, data) {
if (data.nodes.length !== 1){
// do stuff
setData({ nodes: data.nodes, links: data.links });
}else{
if (!isDataInittiallized){
setData({ nodes: newData.nodes, links: newData.links });
isDataInittiallized = 1;
}
}
}
...
useEffect(() => {
...
window.api.receive("file", (bytesArray) => {
var newData = JSON.parse(String.fromCharCode.apply(null, bytesArray));
updateData(newData); // Check function bellow
});
...
}, []);
}
updateData(newData) (a function inside my components fuction)
isDataInittiallized is a variable inside the component that the change got passed to the emitter
data is my variable from the useState() function that the change did NOT got passed to the emitter even though the setData() previously changed the data successfully. So the length remains 1 and it contains the same elements from when it was firstly initialized .
Other info
Have tried to play with passing the data variable to the receive function without any success.
Most probably when the emitter is getting set the function passed (the one that does JSON.parse) is getting passed along and never changed after.

Since the issue is related to stale data inside the updateData function, I suggest to make the following updates:
// Simplified for the sake of brevity
function MyForceGraphComponent() {
const [data, setData] = useState({ nodes: [{ id: 0 }] })
// isDataInitialized needs to be tracked just as any other state
const [isDataInitialized, setIsDataInitialized] = useState(false)
// Wrap `updateData` in `React.useCallback` to prevent stale data
const updateData = useCallback(
(nextData) => {
// skip the update, no need to update the state with existing data
if (isDataInitialized) return;
setData({ nodes: nextData.nodes })
setIsDataInitialized(true) // set to `true` to prevent future updates
},
[isDataInitialized, setIsDataInitialized]
)
const handleReceivedData = useCallback(
(bytesArray) => {
const nextData = JSON.parse(...)
updateData(nextData)
},
[updateData]
)
useEffect(() => {
window.api.receive('file', handleReceivedData);
}, [handleReceivedData])
}
Have a look at this example that mimics what you're trying to do:
CodeSandbox

There some error in you code that i found. When you call updateData(newData), data is null, data.nodes will not work. you can modify you code, "if (data
&& data.nodes && data.nodes.length !== 1)"

Related

ReactQuery does not always mark data as changed when refetching

I am currently trying to use react-query to fetch data for use in a react-table. This is what i currently have, i omitted the table stuff for simplicity:
const { data, refetch } = useQuery(['users'], api.user.getAll);
useEffect(() => {
console.log('data changed')
}, [data]);
// this triggers 'data changed'
const createUser = useMutation((user: IUser) => api.user.create(user), {
onSuccess: () => {
refetch();
console.log('refetched')
},
});
// this does not
const updateUser = useMutation((user: IUser) => api.user.update(user), {
onSuccess: () => {
refetch();
console.log('refetched')
},
});
const onCreateClick = () => {
const newUser: IUser = {
id: 0,
userName: 'test',
email: 'test#mail.de'
}
createUser.mutate(newUser);
};
const onEditClick = (user: IUser) => {
user.userName = 'New Name'
updateUser.mutate(user);
};
console.log(data)
// ... render to table
When adding (or removing) a user everything works as expected. However when i update the data of an existing user the useEffect hook that tracks if data changed does not trigger (and for the same reason the react-table does not show the updated values).
The data does get fetched as expected in both cases and the console.log at the end does log the array with the updated values. It almost seems like the data field returned by useQuery does not get marked as changed for arrays if its length doesn't change.
I don't understand this, since this is new data that got fetched from an api and thus should always get treated as changed.
I am using axios under the hood to do the fetching if that is relevant.
What am i doing wrong, any ideas?
I found the issue:
user.userName = 'New Name'
This was a reference to a user inside of data. Never edit the values in data returned by useQuery in place. By doing this the newly fetched data did match the existing one and thus useQuery did not mark it as changed.

React SWR - how to know that updating (mutating) is running?

Im mostly using SWR to get data, however I have a situation that I need to update data. The problem is, I need an indicator that this request is ongoing, something like isLoading flag. In the docs there's a suggestion to use
const isLoading = !data && !error;
But of course when updating (mutating) the data still exists so this flag is always false. The same with isValidating flag:
const { isValidating } = useSWR(...);
This flag does NOT change when mutation is ongoing but only when its done and GET request has started.
Question
Is there a way to know if my PUT is loading? Note: I dont want to use any fields in state because it won't be shared just like SWR data is. Maybe Im doing something wrong with my SWR code?
const fetcher = (url, payload) => axios.post(url, payload).then((res) => res);
// ^^^^^ its POST but it only fetches data
const updater = (url, payload) => axios.put(url, payload).then((res) => res);
// ^^^^^ this one UPDATES the data
const useHook = () => {
const { data, error, mutate, isValidating } = useSWR([getURL, payload], fetcher);
const { mutate: update } = useSWRConfig();
const updateData = () => {
update(getURL, updater(putURL, payload)); // update data
mutate(); // refetch data after update
};
return {
data,
updateData,
isValidating, // true only when fetching data
isLoading: !data && !error, // true only when fetching data
}
Edit: for any other who reading this and facing the same issue... didnt find any solution for it so switched to react-query. Bye SWR
const { mutate: update } = useSWRConfig();
const updateData = () => {
// this will return promise
update(getURL, updater(putURL, payload)); // update data
mutate(); // refetch data after update
};
By using react-toastify npm module to show the user status.
// first wrap your app with: import { ToastContainer } from "react-toastify";
import { toast } from "react-toastify";
const promise=update(getURL, updater(putURL, payload))
await toast.promise(promise, {
pending: "Mutating data",
success: "muttation is successfull",
error: "Mutation failed",
});
const markSourceMiddleware = (useSWRNext) => (key, fetcher, config) => {
const nextFetcher = (...params) =>
fetcher(...params).then((response) => ({
source: "query",
response,
}));
const swr = useSWRNext(key, nextFetcher, config);
return swr;
};
const useHook = () => {
const {
data: { source, response },
mutate,
} = useSWR(key, fetcher, { use: [markSourceMiddleware] });
const update = mutate(
updateRequest().then((res) => ({
source: "update",
response,
})),
{
optimisticData: {
source: "update",
response,
},
}
);
return {
update,
updating: source === "update",
};
};
Hmm based on that:
https://swr.vercel.app/docs/conditional-fetching
It should work that the "is loading" state is when your updater is evaluates to "falsy" value.
REMAINDER! I don't know react swr just looked into docs - to much time at the end of the weekend :D
At least I hope I'll start discussion :D

React setState inside script text content

Hi guys!
I'm working on the upload image feature into Cloudinary using ReactJS from the client side, then I met a problem.
Description
I added a script tag inside a component with some lines of code inside React.useEffect function. Inside the script tag, I had the data of Cloudinary return when it fetches successfully, and I wanted to get the data by using setState inside the script content.
But I cannot get the data.
Question
How can I use setState inside a string? Or is there any way to get the data that was returned inside a string?
Here is my code.
React.useEffect(() => {
const scriptCloudinaryCdn = document.createElement('script');
scriptCloudinaryCdn.src =
'https://upload-widget.cloudinary.com/global/all.js';
scriptCloudinaryCdn.async = true;
const widgetScript = document.createElement('script');
// I wanted to get the data that was returned from this
const widgetContent = ` var myWidget = cloudinary.createUploadWidget({
cloudName: 'cloudname',
uploadPreset: 'upload_preset'}, (error, result) => {
if (!error && result && result.event === "success") {
// this is the data from Cloudinary, I wanted to carry this with setState
console.log('Done! Here is the image info: ', result.info);
}
}
)
document.getElementById("upload_widget").addEventListener("click", function(){
myWidget.open();
}, false);`;
widgetScript.text = widgetContent;
document.body.appendChild(scriptCloudinaryCdn );
document.body.appendChild(widgetScript);
return () => {
document.body.removeChild(scriptCloudinaryCdn );
document.body.removeChild(widgetScript);
};
}, []);
Thank you guys very much!
You can create some global event-emitter instance (for example using this library), subscribe to an event in react component, and then emit the event from widget's success callback.
If you do not want to use global emitter, you can use DOM event emitter instead, like so:
const [uploadedImageData, setUploadedImageData] = useState(null);
useEffect(() => {
const onImageUplaoded = (event) => {
setUploadedImageData(event.detail);
};
document.getElementById("upload_widget").addEventListener('imageuploaded', onImageUploaded);
return () => {
document.getElementById("upload_widget").removeEventListener('imageuploaded', onImageUploaded);
}
}, []);
and inside of your widget's success callback you can do:
uploadPreset: 'upload_preset'}, (error, result) => {
if (!error && result && result.event === "success") {
const event = new CustomEvent('imageuploaded', { detail: result.info });
document.getElementById("upload_widget").dispatchEvent(event);
}
}
Of course you can think about improvements; this is just one of ideas how to solve your problem.

How to receive SocketIO events in React while using states

This is part of my code, what I want to do is this component at any time can receive a message on any of the conversations. Sending a message triggers a Socket event which triggers this code below, but I can't seem to get the "latest" conversations, as the useEffect only triggers when the component mounts (at that point my conversations array has zero length).
What I was thinking is that I should include "conversations" on the useEffect's dependency but that would create multiple websocket connection, one each time a Socket.io event is triggered because it does change the state. Is this the best solution? Thanks in advance!
const [conversations, setConversations] = useState<Array<Conversations>>([]);
useEffect(() => {
async function getConversations() {
try {
const { data } = await axios.get("/api/conversations/");
if (data.success) {
setConversations(data.details);
}
} catch (err) {}
}
getConversations();
socketInstance.on("connect", () => {
console.log("Connecting to Sockets...");
socketInstance.emit("authenticate", Cookies.get("token") || "");
});
socketInstance.on("ackAuth", ({ success }) => {
console.log(
success
? "Successfully connected to Sockets"
: "There has been an error connecting to Sockets"
);
});
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
(conv: Conversations) => {
return conv.conversationId === data.conversationId;
}
);
modifiedConversation.messages.push({
from: {
firstName: data.firstName,
lastName: data.lastName,
profilePhoto: data.profilePhoto,
userId: data.userId,
},
content: data.content,
timeStamp: data.timeStamp,
});
const updatedConversations = [
...conversations.filter(
(conv) => conv.conversationId !== data.conversationId
),
modifiedConversation,
];
setConversations(updatedConversations);
});
}, []);
While attaching and removing the socket listeners every time conversations changes is a possibility, a better option would be to use the callback form of the setters. The only time you reference the state, you proceed to update the state, luckily. You can change
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
to
socketInstance.on("newMessage", (data) => {
setConversations(conversations => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
You should also not mutate the state, since this is React. Instead of
modifiedConversation.messages.push({
do
const modifiedConversationWithNewMessage = {
...modifiedConversation,
messages: [
...modifiedConversation.messages,
{
from: {
// rest of the object to add

Nuxtjs Vuex not saving changes

I have data between sessions that is saved in window.localStorage. When a new session starts a plugin will grab the data and add it to the store.
// ./store/data.js
export const state = () => ({
data: []
})
export const mutations = {
addItemToData (state, item) {
state.data = state.data.push(item)
},
setData (state, data) {
state.data = data
},
}
// ./store/index.js
import localStorage from '../plugins/localStorage'
export const plugins = [localStorage]
// plugins/localStorage.js
const localStorage = store => {
store.subscribe((mutation, state) => {
if (mutation.type === 'data/addItemToData') {
console.log('saving added item to storage')
window.localStorage.setItem('data', JSON.stringify(state.data.data))
}
})
// called when the store is initialized
if (typeof window !== 'undefined') {
if (window.localStorage.data) {
store.commit('data/setData', JSON.parse(window.localStorage.getItem('data')))
}
}
}
export default localStorage
I've thrown all sorts of console statements in, and they all seem to output what they should. The data is in the localStorage, the mutations are firing, but after all that the data isn't in the store.
Any help would be great, thanks!
several things that do not make sense and that you can resolve
#1
addItemToData (state, item) {
state.data = state.data.push(item)
}
Array.push() does not return anything, so it should be written as
addItemToData (state, item) {
state.data.push(item)
}
#2
in your localStorage.js file, when you initialize the data storage, you are assuming that a data variable exists:
if (window.localStorage.data) { ...
but that will never exist from the code you show, as you are adding the data to another variable
window.localStorage.setItem('cart', ...
either change cart into data or data into cart
#3
If still does not work, I would suspect that your plugin is running before the store is actually initialized, for that, make sure you wait a moment before attempt to fill the store with the localStorage data
something like
...
if (window.localStorage.data) {
setTimeout(() => {
store.commit('data/setData', JSON.parse(window.localStorage.getItem('data')))
}, 500)
}
Working example is available on Netlify and code on GitHub
you can use vuex-persistedstate for saving in localstorage
Array.push Returns the length of the Array after insertion, In your case, it overrides the data Array to Number when you make a mutation. Do like the following..
export const state = () => ({
data: []
})
export const mutations = {
addItemToData (state, item) {
state.data.push(item)
// OR
state.data = [...state.data, item]
},
setData (state, data) {
state.data = data
},
}
And also make sure, you have data in localStorage.
try { // handles if any parsing errors
const data = JSON.parse(window.localStorage.getItem('data')
store.commit('data/setData', data)
} catch(err) {
console.log('JSON parsing error', err)
}

Categories