Promises inside WebWorker (react) throwing babel/webpack error - reactjs

I'm trying to offload some intensive data processing to a WebWorker in a react app. If I call any asynchronous function within the onmessage handler, using promises or async/await, I get:
ReferenceError:
_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_0___default is not defined
Here's my worker:
const DataSyncWorker = () => {
self.doSyncing = async () => {
return null;
};
self.onmessage = e => {
if (!e) return;
console.log(`worker received message in listener callback: ${e.data}`);
self.doSyncing();
self.postMessage(`SYNC_COMPLETE`);
};
};
export default DataSyncWorker;
And the setup file for creating the Worker:
export default class WebWorker {
constructor(worker) {
const code = worker.toString();
const blob = new Blob([`(${code})()`]);
return new Worker(URL.createObjectURL(blob));
}
}
And invoking it:
import DataSyncWorker from './workers/DataSyncWorker';
import WebWorker from './workers/workerSetup';
const App = () => {
const dataSyncWorker = new WebWorker(DataSyncWorker);
dataSyncWorker.postMessage(`START_SYNC`);
dataSyncWorker.addEventListener(`message`, event => {
console.log(`index: message from worker: ${e.data}`);
});
}
If I change doSyncing to not be async, everything works fine. This is a simplified example, which I've confirmed still exhibits the same behavior. But I'm not able to use axios, or any other async function. It's my understanding that this should be possible, and, given the error, I am wondering if my issue is related to babel/webpack. Or perhaps I'm doing something else wrong. Any help is greatly appreciated. Thanks.

I solved this with a few changes:
First, I incorporated worker-loader library, using the inline type import, like:
import DataSyncWorker from 'worker-loader!./workers/DataSyncWorker';
And then, I had to unwrap the inner functions from the containing method of the original implementation, so DataSyncWorker now looks like this:
doSyncing = async () => {
return null;
};
self.onmessage = e => {
if (!e) return;
console.log(`worker received message in listener callback: ${e.data}`);
doSyncing();
self.postMessage(`SYNC_COMPLETE`);
};
The other code remains unchanged, and now everything works.
I believe you could use an alternate approach, involving modifying the webpack.config.js, with these additions to the modules.rules section:
{
test: /\.worker\.js$/,
use: ['worker-loader', 'babel-loader'],
include: [path.join(__dirname, 'src/workers')]
},
and then updating the name of your worker file so it's matched by the test condition, and updating it's import, etc, but I haven't tried that yet, and the inline method seemed simpler.

Related

Why "React hook is called in a function" while exporting simple single function that accesses a useAtom state?

I have this simple function that needs to set a state from jotai. I would like this function to have it's own seperate file or be cleaned up somewhere, since it will be reused. I'm new to React and come from Angular. It's kind of a function in a service Angular wise.
How would you solve this properly in React?
Code:
export const setMetamaskWallet = (): void => {
const [, setWeb3] = useAtom(web3Atom);
const [, setLoading] = useAtom(loadingAtom);
const [wallet, setWallet] = useAtom(walletAtom);
setWeb3(
new Web3(window.ethereum),
)
//Todo: Create check if metamask is in browser, otherwise throw error
const setAccount = async () => {
setLoading(true);
const accounts = await window.ethereum.request(
{
method: 'eth_requestAccounts'
},
);
setWallet(
accounts[0],
);
setLoading(false);
}
if (!wallet) {
setAccount();
}
}
Hooks can only be called in React functional components or other hooks. Here it appears you have called it in a typical function, hence the error. You could package this functionality in a custom hook. It may however be most appropriate to keep this function as it is, and instead of calling hooks within it, pass the relevant data into the function as parameters.

Cypress: Not able to stub with a basic example. What might i be missing?

For some reason, I am not able to stub this here. I have reduced my code to almost exactly this here. This should work according to the docs, but I'm wondering if I'm missing a finer detail of typescript / react hooks? That doesn't feel at all like the case but who knows. Thanks ahead of time if you're reading this and taking up your time. I appreciate what you do. Here's my example:
// connection.ts (the method to stub)
export const connectFn = async () => {
return 'i should not be here'
}
// ./reactHook.ts
import { connectFn } from './connection'
export const useMyHook = () => {
const handleConnect = async () => {
const result = await connectFn()
console.log(result)
// expected: 'i have been stubbed!'
// actual: 'i should not be here
}
return {
handleConnect
}
}
// ./App.ts
export const App = () => {
const { handleConnect } = useMyHook()
return <button onClick={handleConnect}>Connect</button>
}
// ./cypress/integration/connection.spec.ts
import * as myConnectModule from '../../connection'
describe('stub test', () => {
it('stubs the connectFn', () => {
cy.stub(myConnectModule, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
// assertions about the view here...
})
})
I thought I understood the cypress and Sinon docs pretty well. I have read that cypress needs an object to a stub from and not the named export directly - hence the * as an import. I have also tried every which way to export it as well. I used their example from the docs directly to no avail. I then thought it may be a typescript issue but it doesn't seem to be the case. I have reduced my actual code to pretty much this example here. I have even removed everything but the logs for testing and I'm still not getting it.
To stub successfully you need to stub (replace) the same instance.
In reactHook.ts
import { connectFn } from './connection'
if (window.Cypress) {
window.connectFn = connectFn
}
In the test
cy.window().then(win => {
cy.stub(win, 'connectFn').resolves('i have been stubbed!')
cy.get('[data-testid=connect-btn]').click()
...
}}
BTW useMyHook implies a React hook, if so you may need a cy.wait(0) to release the main JS thread and allow the hook to run.

How to update a React/Recoil state array from web bluetooth subscription callbacks?

I am new to using React and Recoil and want to display real-time charts (using D3) from data that is gathered in real-time using the Web Bluetooth API.
In a nutshell, after calling await myCharacteristic.startNotifications() and myCharacteristic.addEventListener('characteristicvaluechanged', handleNotifications), the handleNotifications callback is called each time a new value is notified from a Bluetooth device (see this example).
I am using hooks and tried to modify a recoil state from the callback (this was simplified to the extreme, I hope it is representative):
export const temperatureState = atom({
key: 'temperature',
default: 0
})
export function BluetoothControls() {
const setTemperature = useSetRecoilState(temperatureState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
setTemperature(event.target.value.getInt16(0))
}
}
return <button onClick={nofity}/>Start notifications</button>
}
This work fine if I want to display the latest value somewhere in the app. However, I am interested in keeping the last few (let's say 10) values in a circular buffer to draw a D3 chart.
I tried something along the lines of:
export const temperatureListState = atom({
key: 'temperature-list',
default: []
})
export function BluetoothControls() {
const [temperatureList, setTemperatureList] = useRecoilState(temperatureListState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
let temperatureListCopy = temperatureList.map(x => x);
temperatureListCopy.push(event.target.value.getInt16(0))
if (temperatureListCopy.length > 10)
temperatureListCopy.shift()
setTemperatureList(temperatureListCopy)
}
}
return <button onClick={nofity}/>Start notifications</button>
}
However, is is pretty clear that I am running into the issue described here where the function is using an old version of temperatureList that is captured during render. As a result, temperatureState is always empty and then replaced with a list of one element.
How to maintain a consistent list in a React state/Recoil atom that is updated from an external callback? I think this issue is a bit similar but I'd like to avoid using another extension like Recoil Nexus.
useSetRecoilState accepts an updater function as an argument with the value to be updated as the first parameter:
export function BluetoothControls() {
const setTemperatureList = useSetRecoilState(temperatureListState);
const notify = async () => {
...
temperatureCharacteristic.addEventListener('characteristicvaluechanged', event => {
setTemperatureList(t => {
let temperatureListCopy = t.map(x => x);
temperatureListCopy.push(event.target.value.getInt16(0))
if (temperatureListCopy.length > 10)
temperatureListCopy.shift()
return temperatureListCopy
})
}
}
return <button onClick={nofity}/>Start notifications</button>
}
This solves the issue as the updater function is only evaluated on events.

How do I read from local storage in Ionic React?

I'm new to React and Ionic as well as JS in general so I've followed the tutorial and tried to adapt it to my use case.
What I want to achieve is the following:
read a JSON string from local storage when the app loads
write the (newer) JSON back to storage when the app quits
What I have right now is (heavily truncated):
ExploreContainer.tsx
import { useStore } from '../hooks/useFilesystem';
var vars = { intervaltime : 50000 /* ... */ };
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
const { writeVars, readVars } = useStore();
writeVars();
if ( vars.intervaltime == 50000 ) {
vars = JSON.parse( readVars() );
}
console.log( vars );
// ...
useFilesystem.ts
import { useStorage } from '#ionic/react-hooks/storage';
import { vars } from '../components/ExploreContainer';
const FILESTORAGE = "files";
export function useStore() {
const { get, set } = useStorage();
const writeVars = async () => {
set(FILESTORAGE, JSON.stringify(vars));
};
const readVars = function() {
get(FILESTORAGE).then((value) => { return value });
}
return {
writeVars,
readVars
};
};
The problem right now:
The readVars() call in the React.FC doesn't wait for the get in the custom hook. When I log the output on read, I see that it's an unfulfilled promise. Naturally, this prompts the JSON.parse() to throw an error, because the string it could parse isn't read yet.
I tried to declare an async function in the React.FC so I could await it, but function declarations are not possible because the FC is in strict mode always.
I also tried to declare the async function before the React.FC but this doesn't work as well, because I mustn't call the hook outside the FC if I interpreted the error messages correctly.
In the custom hook I previously had an await statement instead of the .then, but the behavior was the same.
If there is an easier way to read and write a JSON string into some form of persistent(!) storage, I'd be happy to learn about it!
I see this as classic async/await issue only.
As you declared function 'writeVars' as async, so I expect function 'set()' is async in nature. If so please add await in front of 'set' function.
If 'get' function also an async one, then add await in front of it and add 'async' to 'readVars' function declaration.
If point 1 and point 2 are correct, then add await in front of functions 'writeVars' and 'readVars' where ever you call them and add async to the function declaration of calling function.
The problem was not being able to await in the React.FC-block.
However, the following worked like a charm:
var readPromise = readVars();
readPromise.then(function(result) {
vars = JSON.parse(result);
//doing stuff to the data
});
This ensured that the JSON.parse(result) only executes when the readVars-promise has been successfully resolved.

React and firebase adding document to collection issue - Warning: Can't perform a React state update on an unmounted component

I'm trying to add a document to a firebase collection. It works and adds the document but i get the following warning in my react console -
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
This is the utility function I wrote
export const addDocument = async (title) => {
const collectionRef = firestore.collection('collections')
const newDocRef = collectionRef.doc()
const snapshot = await newDocRef.get()
if(!snapshot.exists) {
try {
await newDocRef.set({
title,
items: []
})
} catch (e) {
console.error('Error creating document.', e.message)
}
}
}
This is the component that i'm calling the utility in and is giving me the error
const AdminPage = () => {
const handleSubmit = () => {
addDocument('hat')
}
return (
<div>
<button onClick={handleSubmit}>Add Collection</button>
</div>
)
}
All my imports and exports are fine i've just omitted it to make things more concise.
The code contains async operations so you should implement method: componentWillUnmount
As your code awaits for all async operation (get and set only I think) it should not be a case in your example. This is just a warning and according to my understanding it is showing that async operation may end when object is already destroyed.
Passing some interesting articles and questions:
one, two, three, four
if you need more there is tones of articles over the internet.
I hope it will help!

Resources