I'm using React and Node to build an web-based interface to modify data in a Firebase database. I've previously used the Firebase Web SDK in this app to load data from the database, but I've encountered a strange issue with saving a user's changes. When I call set on a database reference (i.e. firebase.database().ref('/path/to/object').set({abc: 'xyz'})), the webpage hangs. Oddly enough, the changes are saved to the database, but the callbacks specified with then are never called (depending on the browser, a This page is slowing down your browser message appears). I'm certain that the issue is related to set as removing the call removes the hang (see save() in my code below).
import React from 'react'
import * as firebase from 'firebase'
// additional (unrelated) imports
export default class Editor extends React.Component {
constructor(props) {
super(props)
this.state = {
savingModal: false,
errorModal: false,
cancelModal: false,
errors: []
}
}
save() {
// this.form is a Reactstrap Form
// validate is a function that returns an array of strings
var errors;
// validate the form, show the errors if any
if ((errors = this.form.validate()) && errors.length > 0)
this.setState({errorModal: true, errors: errors})
else {
// collect is a function that returns an object with the data that the user entered
var x = this.form.collect()
// getEditorInfo is a function that returns info such as the type of object being edited
var info = this.getEditorInfo()
firebase.database().ref(`/${info.category}/${x.id}/`).set(x).then(() => {
this.closeEditor()
}, e => {
alert(`An unexpected error occurred:\n${e}`)
})
this.setState({savingModal: true})
}
}
// closes the window or redirects to /
closeEditor() {
if (window.opener)
window.close()
else
window.location.href = '/'
}
render() {
// BasicModal is a custom component that renders a Reactstrap Modal
// IndeterminateModal is a custom component that renders a Reactstrap Modal with only a Progress element
// EditorToolbar and EditorForm are custom components that render the UI of the page (I don't think they're relevant to the issue)
var info = this.getEditorInfo()
if (!info)
return <BasicModal isOpen={true} onPrimary={this.closeEditor} primary="Close" header="Invalid Request" body="ERROR: The request is invalid."/>
else
return <div>
<EditorToolbar onSave={this.save.bind(this)} onCancel={() => this.setState({cancelModal: true})}/>
<EditorForm ref={f => this.form = f}/>
<BasicModal toggle={() => this.setState({cancelModal: !this.state.cancelModal})} isOpen={this.state.cancelModal} header="Unsaved Changes" body={<p>If you close this window, your changes will not be saved.<br/>Are you sure you want to continue?</p>} primary="Close Anyway" primaryColor="danger" secondary="Go Back" onPrimary={this.closeEditor}/>
<IndeterminateModal style={{
top: '50%',
transform: 'translateY(-50%)'
}} isOpen={this.state.savingModal} progressColor="primary" progressText="Processing..."/>
<BasicModal toggle={() => this.setState({errorModal: false, errors: []})} isOpen={this.state.errorModal} header="Validation Error" body={<div><p>Please resolve the following errors:<br/></p><ul>{(this.state.errors || []).map(e => <li key={e}>{e}</li>)}</ul></div>} primary="Dismiss" primaryColor="primary"/>
</div>
}
}
UPDATE 1/8/2018
I came across this article today and I decided to try a new solution involving JavaScript's setTimeout method. In my situation, the freeze occurred after calling this.setState in my app then calling firebase.database().ref(path).set(data). I suspect the freezing issue was caused by this. I guess JavaScript couldn't handle the state change and Firebase operation all at once. This new solution is functional, more secure, faster, and simpler. Take a look:
// to perform your desired Firebase operation:
var timeout = 50 // give JS some time (e.g. 50ms) to finish other operations first
setTimeout(() => firebase.database().ref(path).set(data).then(
() => {/* ... */},
error => {/* ... */}),
timeout)
OLD SOLUTION
I ended up finding a solution. I'm sure it could be improved, but it works. I used the Web Workers API to run my Firebase code.
Create a new JavaScript file in your public folder (Node.js)
Download a copy of the Firebase web SDK source and place it in public
I chose to communicate with my Worker with postMessage
FirebaseWorker.js
self.onmessage = event => {
importScripts('./firebase.js') // import the local Firebase script
firebase.initializeApp({/* your config */})
const promise = p => p.then(
() => self.postMessage({error: null}),
e => self.postMessage({error: e})
const doWork = () => {
switch (event.data.action) {
case 'get':
promise(firebase.database().ref(event.data.path).once('value'))
break;
case 'set':
promise(firebase.database().ref(event.data.path).set(event.data.data))
break;
case 'update':
promise(firebase.database().ref(event.data.path).update(event.data.data))
break;
case 'remove':
promise(firebase.database().ref(event.data.path).remove())
break;
}
}
if (!firebase.auth().currentUser)
firebase.auth().signInWithEmailAndPassword(event.data.email, event.data.password).then(() => doWork())
else
doWork()
}
To use the Worker:
var worker = new Worker('FirebaseWorker.js')
worker.onmessage = event => {
if (event.data.error)
alert(event.data.error)
// ...
}
worker.postMessage({
data: {/* your data (required if set or update is used) */},
path: '/path/to/reference',
action: 'get, set, update, or remove',
email: 'someone#example.com',
password: 'password123'
})
Related
I'm working on an extension that is supposed to extract information from the DOM based specific classes/tags,etc, then allow the user to save the information as a CSV file.
I'm getting stuck on a couple of places and haven't been able to find answers to questions similar enough.
Where I am tripped up at is:
1) Making sure that the page has completely loaded so the chrome.tabs.query doesn't return null a couple of times before the promise actually succeeds and allows the blocksF to successfully inject. I have tried placing it within a settimeout function but the chrome api doesn't seem to work within such the function.
2) Saving the extracted information so when the user moves onto a new page, the information is still there. I'm not sure if I should use the chrome.storage api call or simply save the information as an array and keep passing it through. It's just text, so I don't believe that it should take up too much space.
Then main function of the background.js is below.
let mainfunc = chrome.tabs.onUpdated.addListener(
async(id, tab) => {
if (buttonOn == true) {
let actTab = await chrome.tabs.query({
active: true,
currentWindow: true,
status: "complete"
}).catch(console.log(console.error()));
if (!actTab) {
console.log("Could not get URL. Turn extension off and on again.");
} else {
console.log("Tab information recieved.")
};
console.log(actTab);
let blocksF = chrome.scripting.executeScript({
target: { tabId: actTab[0]['id'] },
func: createBlocks
})
.catch(console.error)
if (!blocksF) {
console.log("Something went wrong.")
} else {
console.log("Buttons have been created.")
};
/*
Adds listeners and should return value of the works array if the user chose to get the information
*/
let listenersF = chrome.scripting.executeScript({
target: { tabId: actTab[0]['id'] },
func: loadListeners
})
.catch(console.error)
if (!listenersF) {
console.log("Listeners failed to load.")
} else {
console.log("Listeners loaded successfully.")
};
console.log(listenersF)
};
});
Information from the DOM is extracted through an event listener on a div/button that is added. The event listener is added within the loadListeners function.
let workArr = document.getElementById("getInfo").addEventListener("click", () => {
let domAr = Array.from(
document.querySelectorAll(<class 1>, <class 2>),
el => {
return el.textContent
}
);
let newAr = []
for (let i = 0; i < domAr.length; i++) {
if (i % 2 == 0) {
newAr.push([domAr[i], domAr[i + 1]])
}
}
newAr.forEach((work, i) => {
let table = document.getElementById('extTable');
let row = document.createElement("tr");
row.appendChild(document.createElement("td")).textContent = work[0];
row.appendChild(document.createElement("td")).textContent = work[1];
table.appendChild(row);
});
return newAr
I've been stuck on this for a couple of weeks now. Any help would be appreciated. Thank you!
There are several issues.
chrome methods return a Promise in MV3 so you need to await it or chain on it via then.
tabs.onUpdated listener's parameters are different. The second one is a change info which you can check for status instead of polling the active tab, moreover the update may happen while the tab is inactive.
catch(console.log(console.error())) doesn't do anything useful because it immediately calls these two functions so it's equivalent to catch(undefined)
Using return newArr inside a DOM event listener doesn't do anything useful because the caller of this listener is the internal DOM event dispatcher which doesn't use the returned value. Instead, your injected func should return a Promise and call resolve inside the listener when done. This requires Chrome 98 which added support for resolving Promise returned by the injected function.
chrome.tabs.onUpdated.addListener(onTabUpdated);
async function onTabUpdated(tabId, info, tab) {
if (info.status === 'complete' &&
/^https?:\/\/(www\.)?example\.com\//.test(tab.url) &&
await exec(tabId, createBlocks)) {
const [{result}] = await exec(tabId, loadListeners);
console.log(result);
// here you can save it in chrome.storage if necessary
}
}
function exec(tabId, func) {
// console.error returns `undefined` so we don't need try/catch,
// because executeScript is always an array of objects on success
return chrome.scripting.executeScript({target: {tabId}, func})
.catch(console.error);
}
function loadListeners() {
return new Promise(resolve => {
document.getElementById('getInfo').addEventListener('click', () => {
const result = [];
// ...add items to result
resolve(result);
});
});
}
Context
I'm trying to build an app using the following stack:
Vue.js
Electron
I've developed simple Vue apps for a while now and I understand the basics.
What I'm trying to do here is to combine Vue and Electron using this awesome tool: Vue CLI Plugin Electron Builder.
While I've successfully managed to code simple apps with this stack, I'm facing issues when it comes to exploit Electron's nodeIntegration which is supposed to give Node access directly to my Vue code.
Issue
My asynchronous calls to some methods of the systeminformation library seem to be stuck (sometimes).
That is, when I want to assign a value my view's data by calling an asynchronous method of the systeminformation library, Vue seems to hang and await indefinitely.
The weired thing is that, sometimes, when I force refresh the page, I can briefly see in my console the log supposed to be output when the method returns data.
It's almost like refreshing the page forces Vue or something else to update.
I'd suspect the following leads but I might be wrong:
Vue's reactivity issue in my way of declaring and/or assigning.
Node integration issue making Vue+Node not "bounded" properly.
Electron's packaging of a Node library missing configuration to be used in Vue "as if it was a regular JS library".
The latter point is unclear to me as I've always used Electron+Vue to access Node libraries the same way I'd do for web libraries.
There might more to it that could lead to these issues but this is as far as my knowledge goes...
Steps to reproduce
You can create a Vue+Electron as follows:
vue create myapp
vue add electron-builder
yarn add systeminformation
I'm leaving default values for Vue CLI's interactive project creation as they don't have an impact on my issue.
As per Vue CLI Plugin Electron Builder's documentation on nodeIntegration, my vue.config.js file looks like this:
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
},
},
};
You can then use the snippets in Scenario 1 and Scenario 2.
Scenario 1
Let's consider the following example where I assign a value using Axios:
<template>
<pre>{{ test }}</pre>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
test: null,
};
},
async mounted() {
console.log("Getting data from Axios...");
// Assign a value to the "test" data with an asynchronous HTTP call with Axios.
this.test = (
await axios.get("https://cat-fact.herokuapp.com/facts")
).data;
// It gets there as soon as the server responds.
console.log("Got it!", this.test);
},
};
</script>
Here, the asynchronous call works as expected, my test data updates properly and shows in my view as soon as the Axios call got an answer.
Scenario 2
Now, if I use the same logic to get my data from a method of the systeminformation library:
<template>
<pre>{{ test }}</pre>
</template>
<script>
import systeminformation from "systeminformation";
export default {
data() {
return {
test: null,
};
},
async mounted() {
console.log("Getting data from systeminformation...");
// Assign a value to the "test" data with an asynchronous call to a method of systeminformation.
this.test = await systeminformation.getStaticData();
// It just won't get there most of the time...
console.log("Got it!", this.test);
},
};
</script>
In that case, my view won't show the test data as Vue seems to hang on the systeminformation call indefinitely.
The console log won't even show as the await statement appears to make the mounted hook stuck.
I just tested systeminformation.getStaticData() function in my own Electron + Vue app and here's results:
It executes code asynchorously but the operation is so heavy, it makes the app almost completely unresponsive. It repeatedly spawns ~30 Node child processes effectively blocking 3-4 CPU threads.
I think it might be a bug with getStaticData() function. You might want to create a new issue in their repo and report it.
Solution
Either don't use this particular function and get all the needed info by running other systeminformation functions.
OR
Enable node integration in workers:
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true
}
Execute this function in a worker, rather than on the main thread, and send the results back to your component, then terminate the worker. Though when you do this, open task manager and make sure it terminates all the spawned child processes with it:
Vue component
import sysInfoWorker from 'worker-loader!./workers/sysInfoWorker'
data () {
return {
sysInfoWorker: null
}
},
mounted () {
this.initSysInfoWorker()
},
methods: {
initSysInfoWorker () {
this.sysInfoWorker = new sysInfoWorker()
try {
this.sysInfoWorker.onmessage = (event) => {
console.log('sysInfoWorker message:', event)
this.test = event
}
this.sysInfoWorker.onerror = (error) => {
console.log('sysInfoWorker error:', error)
}
}
catch (error) {
console.log(error)
}
},
startSysInfoWorker () {
this.sysInfoWorker.postMessage({
operation: 'run:getStaticData'
})
},
cancelSysInfoWorker () {
this.sysInfoWorker.postMessage('cancel:getStaticData')
// this.sysInfoWorker.terminate()
}
}
Worker
const systeminformation = require('systeminformation')
const state = { cancelled: false }
// Listen to messages from parent thread
self.addEventListener('message', (event) => {
if (event.data === 'cancel:getStaticData') {
state.cancelled = true
}
else {
state.cancelled = false
initWorker(event)
}
})
async function initWorker (event) {
if (event.data.operation === 'run:getStaticData') {
const info = await systeminformation.getStaticData()
self.postMessage({ info: info })
}
}
If the worker throws errors, try adding the following to your vue.config:
module.exports = {
configureWebpack: {
module: {
rules: [
{
test: /\.worker\.js$/,
use: { loader: 'worker-loader' }
}
I have a Node.js function utilizing sockets on another React page which is working correctly removing users from a queue. I'm trying to implement that function on another page but I keep getting func.call is not a function. Here's the problematic React:
class Stations extends Component {
static propTypes = {
className: PropTypes.string,
queue: PropTypes.array,
Remove: PropTypes.func,
}
Remove = (station, index) => {
this.socket.on('removeUser', {station, index});
}
render() {
const { className, queue } = this.props
return (
...
<Button as={Label} color="red" className="deleteLabel" size="tiny" onClick="{this.Remove(station, index)}">Remove</Button>
...
onClick should be a function:
onClick={() => this.Remove(station, index)}
Additionally, if you meant to send a message over socket instead of adding a new event listener, you probably meant emit:
this.socket.emit('removeUser', {station, index});
I'm trying to change the root of my app to render a different Router if the user authenticates. (public marketing website then the SAAS app when logged in).
My current code:
class MainRouter extends React.Component {
render() {
if(isAuthenticated()) {
return <AppRouter/>
} else {
return <FrontRouter/>
}
}
}
This works but I have to refresh the browser after authenticating or logging out otherwise it throws errors trying to render components that require a user.
isAuthenticated:
export const isAuthenticated = () => {
if(typeof window == 'undefined') {
return false
}
if(localStorage.getItem('jwt')) {
return JSON.parse(localStorage.getItem('jwt'))
} else {
return false;
}
};
Consider using state (ideally with hooks, or Redux if you are comfortable) to hold the JWT in memory. The idea is to store it immediately after logging in and have it available to use through component (or Redux) state. Then upon refreshing the page, state will still load from localState or cookies.
A secondary point, LocalStorage can act differently across different browsers, and even more so with privacy settings. Consider using cookies rather than localStorage as its usage is more predictable and consistent, plus the security risk is more or less the same. (code example below)
Third, I highly recommend switching from class-based components to function-based components to take advantage of React's new Hooks API. It is a much simpler way to manage state and saves you many lines of code.
// library imports
import React from 'react';
// component imports
import AppRouter from './AppRouter';
import FrontRouter from './FrontRouter';
// main
export default function MainRouter({isAuthenticated}) {
return isAuthenticated ? <AppRouter/> : <FrontRouter/>
}
If "isAuthenticated" is a boolean variable created by React's "useState" function from parent component, you can pass that variable to "MainRouter" component and conditionally render "AppRouter" or "FrontRouter" (I'm using a ternary operator instead of an "If/Else" statement to save lines)
In this case, the parent component file would look like this:
// in ParentComponent.jsx
// library imports
import React, {useState} from 'react';
// component imports
import MainRouter from './MainRouter';
// main component
export default function ParentComponent() {
// set state here
const defaultState = false;
const [isAuthenticated, setIsAuthenticated] =useState(defaultState);
return (
<div className="ParentComponent" >
<MainRouter isAuthenticated={isAuthenticated} />
</div>
)
}
helper function for getting cookies from w3schools (https://www.w3schools.com/js/js_cookies.asp)
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
I am creating some unit test for my component, but the test keeps failing, since the button I'm testing
keeps not getting triggerd by a click-event.
I've used the docs as a foundation for my test: https://vuetifyjs.com/sv-SE/getting-started/unit-testing/
I've also tried some of the suggestions mentioned here: https://forum.vuejs.org/t/how-to-trigger-an-onchange-event/11081/4
But it seems like I'm missing something, anyone who can help me out?
My test:
test('If you can click on the Test button', () => {
const wrapper = shallowMount(myComponent, {
localVue,
vuetify,
});
const event = jest.fn();
const button = wrapper.find({name: 'v-btn'})
expect(button.exists()).toBe(true) //this works
wrapper.vm.$on('v-btn:clicked', event)
expect(event).toHaveBeenCalledTimes(0)
button.trigger('click')
expect(event).toHaveBeenCalledTimes(1)
})
myComponent:
<template>
<v-btn class="primary-text" #click.native="methodForTesting($event)">Test</v-btn>
<template>
<script>
methods: {
methodForTesting(){
console.log('button clicked!')
}
</script>
Hope this help, I changed your HTML a bit.
Firstly, I added a <div> and put <v-btn> inside it, this is very
important.
Secondly, I declared a data prop called index which is
initialized in 1.
Thirdly, I used data-testid="button" to identify
it and find it during test.
<template>
<div>
<v-btn data-testid="button" class="primary-text" #click="methodForTesting">Test</v-btn>
</div>
</template>
<script>
export default {
data() {
return {
index: 1
};
},
methods: {
methodForTesting() {
this.index++;
}
}
};
</script>
Now, for the unit test.
The key is to use vm.$emit('click') instead of .trigger('click') since v-btn is a component of Vuetify. If you were using button tag, then you can use .trigger('click').
Also, I changed how jest finds this button.
import Vuetify from 'vuetify'
// Utilities
import { mount, createLocalVue } from '#vue/test-utils'
// Components
import Test from '#/views/Test.vue';
// VARIABLES INITIALIZATION //
const vuetify = new Vuetify()
const localVue = createLocalVue()
// TESTING SECTION //
describe('Testing v-btn component', () => {
it('should trigger methodForTesting', async () => {
const wrapper = mount(Test, {
localVue,
vuetify,
})
const button = wrapper.find('[data-testid="button"]')
expect(button.exists()).toBe(true)
expect(wrapper.vm.$data.index).toBe(1)
button.vm.$emit('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.$data.index).toBe(2)
})
})
Now, when you are doing a unit test, you should check inputs and outputs. In this case, your input is the click event and your output, is not your method been called, but the data modified or sent by this method. That's why I declared index to see if it changes when you click the button.
Anyway, if you want to check if your method was called, you can use this code instead
describe('Testing v-btn component', () => {
it('should trigger methodForTesting', async () => {
const methodForTesting = jest.fn()
const wrapper = mount(Test, {
localVue,
vuetify,
methods: {
methodForTesting
}
})
const button = wrapper.find('[data-testid="button"]')
expect(button.exists()).toBe(true)
expect(methodForTesting).toHaveBeenCalledTimes(0)
button.vm.$emit('click')
await wrapper.vm.$nextTick()
expect(methodForTesting).toHaveBeenCalledTimes(1)
})
})
But you will receive the next error:
[vue-test-utils]: overwriting methods via the `methods` property is deprecated and will be removed in the next major version. There is no clear migration path for the `methods` property - Vue does not support arbitrarily
replacement of methods, nor should VTU. To stub a complex method extract it from the component and test it in isolation. Otherwise, the suggestion is to rethink those tests.
This is my first post btw