How to call React component function from Electron main.js using contextBridge - javascript

As the title states, I would like to call a function defined in one of my React components from the Electron main.js file. I know how to do it the other way around, where for example the user clicks a button and it calls a function in main.js and then returns to the React component. I need it to be the opposite, where main.js calls the React function and then returns the result to main.
I cannot simply have all the functionality in main.js since I cannot pass DOM objects to main from React (it has resulted in the error "object cannot be cloned"). So I figure it shouldn't be too hard to just send the request to React, have the React side of my app do the stuff, and then return the resulting string back to main.
I have found a number of other similar posts here where various solutions are offered, but none specifically for this exact problem. #1 exposes ipcRenderer which is a bad security practice I can't afford. #2 and #3 only explain it for vanilla Javascript, not React components. And while #4 does deal with React, it does not deal with Electron, and it is designed for React classes (I'm using functional components).
My resulting attempt looks like this mess:
const [mounted, setMounted] = useState(false)
if(!mounted){
window.reactFunction = myFunction;
}
useEffect(() =>{
setMounted(true)
},[])
window.electron.receive("fromMain", async () => {
return myFunction();
});
async function myFunction()
{
return "result";
}
However, the function instead gets called during rendering and is not called by the contextBridge. Here is my preload.js:
contextBridge.exposeInMainWorld('electron', {
receive: async (channel, func) => {
let validChannels = ["fromMain"];
if (validChannels.includes(channel)) {
return await ipcRenderer.on("fromMain", (event, ...args) => func(...args));
}
}
});
For some reason the preload.js script is not executed. And here is main.js:
const result = window.webContents.send('fromMain');
I have also found this discussion where it is made clear the security issues associated with calling ipcRenderer directly and so I would like to use contextBridge (which I'm already using for calling main functions from React anyway). It's also where my preload.js code comes from. But that example doesn't use React either.
Is this even possible? Or am I supposed to go about this problem a different way?

Looks like I made a typing mistake on this line:
return await ipcRenderer.on("fromMain", (event, ...args) => func(...args));
In this line I'm returning the function, so the function itself is not getting called. Remove the "return" and it will work. "Await" doesn't seem to make a difference.
However, once you do call the function, it can't actually return anything. So you need to make another call from renderer back to main, like this:
window.electron.receive("fromMain", async () => {
const returnValue = await myFunction();
window.electron.sendReturnValue(returnValue);
});
async function myFunction()
{
return "result";
}
Unless there is a still better way of doing it, but this solves my issue for now, and hopefully helps others.

Related

How to use hooks within function in React Js

I have a file where I keep all my functions that make api calls, then I import these functions inside components to use where needed. The issue I'm facing is that when api call is made and it returns 'unauthorized user'(because token has expired) I need to redirect users to the login page.
// apiCalls.js file
export async function getData(){
let response = await fetch(url)
let res = response.json()
// here I need to add redirect to /login if res.status.code is 401 for example
}
I tried to create a custom hook (with useNavigate) to use inside the function, but app throws error saying that hooks can't be used inside function. I can check status of the request inside the component(after I import function from apiCalls.js) but it doesn't seem like a correct way of approaching this as I'll have to add check inside every component that may use this function. Any advise is greatly appreciated
EDITED: to add context, I need to redirect user from a function( not functional component), function is exported from apiCalls.js file.
There's a great way to use hooks inside of a function - make the function a hook! The caveat is that this function will also need to follow the rules of hooks; a big one being the one you've just discovered: you should only be calling it inside a component or other hooks. If you're getting an error because of eslint, you generally also have to (and should) prefix this function with use (like useGetData).
export function useGetData(){
const navigation = useNavigation();
const getData = async (url) => {
let response = await fetch(url)
let res = response.json()
if (/* isInvalidStatus */) {
// navigate to '/login'
}
// return your data
}
return { getData }
}
export function MyComponent() {
const { getData } = useGetData();
// call getData() from useEffect or a click handler
// return jsx;
}
Brief explanation in case it helps:.
First we'll rename the function to follow convention, but we'll also have to remove the async keyword (which we address later). We'll add the useNavigation hook.
export function useGetData() {
// or whatever navigator your router provides
const navigation = useNavigation();
}
The hook itself can't be async, but we can expose a function in the hook's return object:
const getData = async (url) => {
// would probably use const instead of let
const response = await fetch(url);
if (response.status === 401 || response.status === 403) {
navigate('/login');
return;
}
return response.json();
}
return { getData }
And now in the component you can grab getData from useGetData and use it however you want; the auth guard logic will be handled for us in the hook, no matter which component we use it in.
Hooks are JavaScript functions, but you need to follow two rules when using them.
Don’t call Hooks inside loops, conditions, or nested functions.
Don’t call Hooks from regular JavaScript functions.
So if you want to use hook inside a function, change that function into hook

React Hook useEffect has a missing dependency?

I have the below code and as far as I'm concerned it only relies upon [page] yet, I am getting the error
React Hook useEffect has a missing dependency
I've seen similar questions about commenting out a line in your eslint file but
I don't have an eslint file
I would rather understand and resolve the issue
const fetchStarWarsInfo = async () => {
const response = await getData(
`https://swapi.dev/api/people/?page=${dontReturnZero(page)}`
);
dispatch(setCurrentCharacters(response.results));
};
useEffect(() => {
fetchStarWarsInfo();
}, [page]);
Actually it might also ask you to put dispatch in dependencies and other stuff, so it's not so critical not to list all of the dependencies that he ask for.
Try to remove dispatch from your fetchStarWarsInfo and check. If error doesn't disappear, so try to remove also getData function.
You might also want to configure eslint for React, so those errors would have more information about error
If fetchStarWarsInfo is declared within the functional component, everytime state changes, the function is re-constructed and re-declared, therefore, it changes. Therefore the error message is notifying you that although the function is changing, it's not declared as a dependency. The problem though, is if you declare a function as a dependency, you can potentially create an infinite loop, since if you change state within the fetchStarWarsInfo, the useEffect hook will have to continuously keep running, as a new instance of fetchStarWarsInfo will be created.
The solutions to this are pretty straightforward. Declare the fetchStarWarsInfo within the useEffect hook or split up the logic within the function, so you can declare it outside the functional component completely. Then handle state changes within the useEffect hook. That way the function is declared only once, and state changes are handled within the hook, thereby causing no unwanted side effects
After doing this, you need to verify that all the functions used within fetchStarWarsInfo are not continuously changing either. (i.e dontReturnZero,setCurrCharacters, etc). You can check this by commenting them out and seeing if the error disappears. If it does disappear, you'll need to include them in your dependency array for useEffect.
Example 1:
useEffect(() => {
const fetchStarWarsInfo = async () => {
const response = await getData(
`https://swapi.dev/api/people/?page=${dontReturnZero(page)}`
);
dispatch(setCurrentCharacters(response.results));
};
fetchStarWarsInfo();
}, [page, dispatch]);
Example 2:
const fetchStarWarsInfo = async (page) => {
const response = await getData(
`https://swapi.dev/api/people/?page=${dontReturnZero(page)}`
);
return response
};
const FunctionalComponent=() => {
const dispatch = useDispatch()
useEffect(() => {
fetchStarWarsInfo(page).then((data)=> dispatch(setCurrentCharacters(data.results));
}, [page, dispatch]);
}

How to handle useEffect being called twice in strictMode for things that should only be run once? [duplicate]

This question already has answers here:
Why useEffect running twice and how to handle it well in React?
(2 answers)
Closed 5 months ago.
This post was edited and submitted for review 5 months ago and failed to reopen the post:
Original close reason(s) were not resolved
I understand that React calls useEffect twice in strict mode, this question is about asking what the correct way of handling it is.
I have recently hit an issue with React useEffect being called twice in strictMode. I'd like to keep strict mode to avoid issues, but I don't see any good way of making sure some specific effects are only run once. This is in a next.js environment, where I specifically want the code to only run in the client once.
For example, given the following component:
import React, { useState, useEffect } from "react";
import ky from "ky";
const NeedsToRunOnce: React.FC = () => {
const [loading, setLoading] = useState(false);
const doSomethingThatOnlyShouldHappenOnce = (data: any) => {
// Do something with the loaded data that should only happen once
console.log(`This log appears twice!`, data);
};
useEffect(() => {
if (loading) return;
console.log("This happens twice despite trying to set loading to true");
setLoading(true);
const fetchData = async () => {
const data = await ky.get("/data/json_data_2000.json").json();
setLoading(false);
doSomethingThatOnlyShouldHappenOnce(data);
};
fetchData();
}, []);
return <div></div>;
};
export default NeedsToRunOnce;
useEffect will be called twice, and both times loading will still be false. This is because strictMode makes it be called twice with the same state. The request will go through twice, and call doSomethingThatOnlyShouldHappenOnce twice. All the console.log calls above will appear twice in the console.
Since I can't modify the state to let my component know that the request has already started happening, how can I stop the get request from happening twice and then calling code that I only want to be called once? (For context, I am initialising an external library with the data loaded in the useEffect, and this library should only be initialised once).
I found a GitHub issue about this where Dan Abramov says:
Usually you’d want to have some kind of cleanup for your effect. It should either cancel your fetch or make sure you ignore its result by setting a variable inside your effect. Once you do this, there should be no actual difference in behavior.
If you cancel your fetch in effect cleanup, only one request will complete in development. However, it also doesn’t matter if another request fires. It’s being ignored anyway, and the stress-testing only happens in development.
While I agree in principle, this is tedious in practice. I've already hit two different use cases in my application where I have some library call or request that needs to only be done once in the client.
What's a reliable way of making sure that a piece of code in useEffect is only run once here? Using a state to keep track of it having already been run doesn't help, since react calls the component twice with the same state in a row.
You can use a ref to execute your useEffect once (first time or second time, as you wish), or just use a customHook. In my case, i execute the useEffect the second time. Swap true and false, to execute it the first time and not the second one.
import { useEffect, useRef } from "react";
export default function useEffectOnce(fn: () => void) {
const ref = useRef(false);
useEffect(() => {
if (ref.current) {
fn();
}
return () => {
ref.current = true;
};
}, [fn]);
}
And to use it, you can pass your callback function in param :
useEffectOnce(() => console.log("hello"));

How do I make dependent queries in react-query?

I have a component called Sidebar.tsx that is fetching some data here but I want to only fetch levels only if studentId is available. I looked through dependent queries on react-query documentation . I applied that in my code but it looks like it doesn't work. Here's is what I did.
First of all I fetched the studentId from an API too, here is how it looks like:
const studentId = intakeProgramStore.getStudentShipByUserId(authUser?.id)
.data?.data.data[0];
Now another function to get programs
class IntakeProgramStore{
getIntakeProgramsByStudent(studentId: string) {
return useQuery(
['intakeProgram/studentId', studentId],
() => intakeProgramService.getIntakeProgramsByStudent(studentId),
{ enabled: !!studentId },
);
}
}
export const intakeStore = new IntakeProgramStore();
This function is located in stores/intake.store.ts and what it does is that when it gets called it calls a service method called intakeProgramService.getIntakeProgramsByStudent which is also imported at the top and it works fine as expected. The problem I have here is that it is calling that service even if studentId is undefined while it was supposed to call it when it has a value.
Here's how I'm calling it inside Sidebar.tsx which is my UI component
const programs = intakeProgramStore.getIntakeProgramsByStudent(student.id)
.data?.data.data[0];
I moved the code that was in the stores/intake.store.ts file into Sidebar.tsx and now it worked fine but I don't know why. Here's how it looks
const programs = useQuery(
['intakeProgram/studentId', student?.id],
() => intakeProgramService.getIntakeProgramsByStudent(student?.id),
{ enabled: !!student
I'm not sure why it's not working when I'm fetching inside the store but works when I do it in the UI component.
Generally, your code looks correct. The only thing that I can see is that in the first example, you're calling useQuery inside a function called getIntakeProgramsByStudent, which is an invalid hook call. Hooks can only be called:
by functional react components
inside custom hooks, which are functions that start with use....
see also the rules of hooks react documentation.
A custom hook for your useQuery call would be:
export const useIntakeProgramsByStudent(studentId: string) {
return useQuery(
['intakeProgram/studentId', studentId],
() => intakeProgramService.getIntakeProgramsByStudent(studentId),
{ enabled: !!studentId },
);
}

How to mock an asynchronous function call in another class

I have the following (simplified) React component.
class SalesView extends Component<{}, State> {
state: State = {
salesData: null
};
componentDidMount() {
this.fetchSalesData();
}
render() {
if (this.state.salesData) {
return <SalesChart salesData={this.state.salesData} />;
} else {
return <p>Loading</p>;
}
}
async fetchSalesData() {
let data = await new SalesService().fetchSalesData();
this.setState({ salesData: data });
}
}
When mounting, I fetch data from an API, which I have abstracted away in a class called SalesService. This class I want to mock, and for the method fetchSalesData I want to specify the return data (in a promise).
This is more or less how I want my test case to look like:
predefine test data
import SalesView
mock SalesService
setup mockSalesService to return a promise that returns the predefined test data when resolved
create the component
await
check snapshot
Testing the looks of SalesChart is not part of this question, I hope to solve that using Enzyme. I have been trying dozens of things to mock this asynchronous call, but I cannot seem to get this mocked properly. I have found the following examples of Jest mocking online, but they do not seem to cover this basic usage.
Hackernoon: Does not use asychronous calls
Wehkamp tech blog: Does not use asynchronous calls
Agatha Krzywda: Does not use asynchronous calls
GitConnected: Does not use a class with a function to mock
Jest tutorial An Async Example: Does not use a class with a function to mock
Jest tutorial Testing Asynchronous Code: Does not use a class with a function to mock
SO question 43749845: I can't connect the mock to the real implementation in this way
42638889: Is using dependency injection, I am not
46718663: Is not showing how the actual mock Class is implemented
My questions are:
How should the mock class look like?
Where should I place this mock class?
How should I import this mock class?
How do I tell that this mock class replaces the real class?
How do set up the mock implementation of a specific function of the mock class?
How do I wait in the test case for the promise to be resolved?
One example that I have that does not work is given below. The test runner crashes with the error throw err; and the last line in the stack trace is at process._tickCallback (internal/process/next_tick.js:188:7)
# __tests__/SalesView-test.js
import React from 'react';
import SalesView from '../SalesView';
jest.mock('../SalesService');
const salesServiceMock = require('../SalesService').default;
const weekTestData = [];
test('SalesView shows chart after SalesService returns data', async () => {
salesServiceMock.fetchSalesData.mockImplementation(() => {
console.log('Mock is called');
return new Promise((resolve) => {
process.nextTick(() => resolve(weekTestData));
});
});
const wrapper = await shallow(<SalesView/>);
expect(wrapper).toMatchSnapshot();
});
Sometimes, when a test is hard to write, it is trying to tell us that we have a design problem.
I think a small refactor could make things a lot easier - make SalesService a collaborator instead of an internal.
By that I mean, instead of calling new SalesService() inside your component, accept the sales service as a prop by the calling code. If you do that, then the calling code can also be your test, in which case all you need to do is mock the SalesService itself, and return whatever you want (using sinon or any other mocking library, or even just creating a hand rolled stub).
You could potentially abstract the new keyword away using a SalesService.create() method, then use jest.spyOn(object, methodName) to mock the implementation.
import SalesService from '../SalesService ';
test('SalesView shows chart after SalesService returns data', async () => {
const mockSalesService = {
fetchSalesData: jest.fn(() => {
return new Promise((resolve) => {
process.nextTick(() => resolve(weekTestData));
});
})
};
const spy = jest.spyOn(SalesService, 'create').mockImplementation(() => mockSalesService);
const wrapper = await shallow(<SalesView />);
expect(wrapper).toMatchSnapshot();
expect(spy).toHaveBeenCalled();
expect(mockSalesService.fetchSalesData).toHaveBeenCalled();
spy.mockReset();
spy.mockRestore();
});
One "ugly" way I've used in the past is to do a sort of poor-man's dependency injection.
It's based on the fact that you might not really want to go about instantiating SalesService every time you need it, but rather you want to hold a single instance per application, which everybody uses. In my case, SalesService required some initial configuration which I didn't want to repeat every time.[1]
So what I did was have a services.ts file which looks like this:
/// In services.ts
let salesService: SalesService|null = null;
export function setSalesService(s: SalesService) {
salesService = s;
}
export function getSalesService() {
if(salesService == null) throw new Error('Bad stuff');
return salesService;
}
Then, in my application's index.tsx or some similar place I'd have:
/// In index.tsx
// initialize stuff
const salesService = new SalesService(/* initialization parameters */)
services.setSalesService(salesService);
// other initialization, including calls to React.render etc.
In the components you can then just use getSalesService to get a reference to the one SalesService instance per application.
When it comes time to test, you just need to do some setup in your mocha (or whatever) before or beforeEach handlers to call setSalesService with a mock object.
Now, ideally, you'd want to pass in SalesService as a prop to your component, because it is an input to it, and by using getSalesService you're hiding this dependency and possibly causing you grief down the road. But if you need it in a very nested component, or if you're using a router or somesuch, it's becomes quite unwieldy to pass it as a prop.
You might also get away with using something like context, to keep everything inside React as it were.
The "ideal" solution for this would be something like dependency injection, but that's not an option with React AFAIK.
[1] It can also help in providing a single point for serializing remote-service calls, which might be needed at some point.

Categories

Resources