I inherited an app which is bootstrapped in a way that is rather perplexing to me (unlike like the docs or anything I have seen), my task is to create a global loader instead of implementing on each page separately.
I thought this would be simple where I could just manage the state in the store and have a shared "Header" component respond to changes in the "loading" property in the store. Boy was I wrong, it seems as though the way the app is setup with factories I don't have access to the store inside my interceptor.
The code is a bit advanced for me and I would be very appreciative if anyone can explain either how to restructure the code or alternatively how to access the store inside the interceptor.
My main.js looke like this:
import 'babel-polyfill' // IE support
import cssVars from 'css-vars-ponyfill'; // css variables support for IE
import 'innersvg-polyfill'; // svg support for IE
// until here, all polyfills
import "vue-material-design-icons/styles.css";
import VueI18n from 'vue-i18n'
import {localizationFactory} from "./localization";
import {apiFactory, apiPluginFactory} from './api/api';
import {storeFactory} from "./store/store";
import {configServiceFactory} from "./services/configService";
import {Services, Security, Utils} from 'em-common-vue';
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'
import {filtersFactory} from './filters/index';
const appsService = new Services.appsService(process.env);
const loginDetails = {
loginHost: appsService.getLoginStorage()
};
console.log("top of main.js before factory functions", this)
Security.ServiceFactory(loginDetails).then($security => {
console.log("inside secutiy service factor 'then'", this)
Vue.config.productionTip = false;
Vue.use(Utils.EventBusPlugin);
Vue.use($security);
var vInstance = new Vue();
const $api = apiFactory(vInstance, $security);
configServiceFactory($security, $api).then($config => {
Vue.use($config);
Vue.use( apiPluginFactory($api) );
// for now
const store = storeFactory($api, null);
Vue.use(VueI18n);
filtersFactory($config.$service);
localizationFactory($config.$service).then(messages => {
const i18n = new VueI18n({
locale: 'en', // set locale
messages, // set locale messages
});
new Vue({
router,
store,
i18n,
render: h => h(App),
computed: {
title: {
set(val) {
document.querySelector('title').innerText = val;
},
get(val) {
return document.querySelector('title').innerText;
}
}
},
mounted() {
cssVars({
silent: false, // default,
watch: true,
onWarning() {
console.log("onWarning",arguments); // 1
},
onError() {
console.log("onError", arguments); // 1
}
});
if (!document.querySelector('title')) {
let title = document.createElement('title');
document.head.append(title);
}
this.title = this.$config.get().title;
}
}).$mount('#app')
});
});
}).catch(err => {
console.error(err);
if (err.loginUrl) {
const nextUrl = appsService.getLogin(window.location.href);
window.location.href = nextUrl;
} else { // for now
alert(err);
}
});
The apiFactory which is where the interceptor resides looks like this:
import axios from 'axios';
import {UsersApi} from "./usersApi";
import {PartnersApi} from "./partnersApi";
import {TrialsApi} from "./trialsApi";
export function apiFactory(vue, $security) {
console.log("inside api factory")
console.log(this)
const $http = axios.create({
baseURL: process.env.VUE_APP_API_URL
});
$http.interceptors.request.use(function (config) {
console.log("inside interceptor ", this)
// this.$store.commit('toggleLoader')
const token = $security.loginFrame.getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, function (error) {
vue.$eventBus.$emit('toastMessageHandler', {message: error.message, type: 'error'});
return Promise.reject(error);
});
$http.interceptors.response.use(function (response) {
return response;
}, function (error) {
if (error.response && error.response.status === 401) {
vue.$eventBus.$emit('authErr', error);
} else if (error.response && error.response.status === 403) {
alert('You are not authorized to access this application. If you believe you are seeing this message in error, please contact support#emergingmed.com.');
}
else if (error.response && error.response.status !== 409){
vue.$eventBus.$emit('toastMessageHandler', {message: error.message, type: 'error'});
}
else if (error.response && error.response.status === 500){
vue.$eventBus.$emit('toastMessageHandler', {message: 'There was a problem with this operation - please contact support#emergingmed.com.', type: 'error'});
}
return Promise.reject({error});
})
const $api = {
users: new UsersApi($http),
partners: new PartnersApi($http),
trials: new TrialsApi($http)
};
return $api;
}
// register $api as vue plugin
export function apiPluginFactory($api) {
return {
install(vue) {
vue.prototype.$api = $api;
},
};
}
When I try to run this code this.$store.commit('toggleLoader') (commented out in the actual code) I get an error that store is undefined. In general this structure does not seem to me idiomatic to vue (or maybe even JS) but I am not sure how I would restructure it.
1)How might I access my store in the interceptor code?
2)How might the app be restructured?
Related
I have followed the code from the official docs. When you scroll down to the full code, it has two things that make problems. First of all, this seems weird:
const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
const auth = getAuth();
They define auth after using it for recaptchaVerifier. But that seems like a typo, so I just switched these two lines.
But I cannot resolve the second issue. Their code is in JavaScript, my code is in TypeScript. They use undefined as an argument in the definition of recaptchaVerifier:
const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
The second argument of the constructor is undefined. Since TypeScript does not allow that, I tried many things, for example these:
const undef: any = undefined; const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undef, auth);
const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', { size: 'invisible' }, auth);
But it ALWAYS gives this error in the console:
ERROR FirebaseError: Firebase: Error (auth/argument-error).
at createErrorInternal (index-0bb4da3b.js:474:41)
at _assert (index-0bb4da3b.js:480:15)
at new RecaptchaVerifier (index-0bb4da3b.js:7369:9)
I could not find anything that helped me fix this error in the internet.
Here is my full code:
LogIn(email: string, password: string) {
const auth = getAuth();
const undef: any = undefined;
const recaptchaVerifier = new RecaptchaVerifier(
'recaptcha-container-id',
undef,
auth
);
/* It never reaches this code below here since new RecaptchaVerifier() always throws an error */
return signInWithEmailAndPassword(auth, email, password)
.then((result) => {
this.afAuth.authState.subscribe((user) => {
if (user) {
this.router.navigate(['home']);
}
});
})
.catch((error) => {
if (error.code == 'auth/multi-factor-auth-required') {
// The user is a multi-factor user. Second factor challenge is required.
const auth = getAuth();
let resolver = getMultiFactorResolver(auth, error);
const phoneInfoOptions = {
multiFactorHint: resolver.hints[0],
session: resolver.session
};
// Send SMS verification code.
const phoneAuthProvider = new PhoneAuthProvider(auth);
phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
.then((verificationId) => {
// verificationId will be needed for sign-in completion.
// Ask user for the SMS verification code via prompt (yeah, very bad UI)
const verificationCode = prompt("Enter the verification code we sent to your number");
if (verificationCode !== null) {
const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
// Complete sign-in.
return resolver.resolveSignIn(multiFactorAssertion);
} else {
this.toast.error("Entered wrong code");
return null;
}
})
.then((userCredential) => {
// User successfully signed in with the second factor phone number.
this.toast.success("Code is correct. Logged in");
this.afAuth.authState.subscribe((user) => {
if (user) {
this.router.navigate(['home']);
}
});
})
.catch((error) => {
console.log(error);
// failed
this.toast.error(error.message);
});
} else if (error.code == 'auth/wrong-password') {
this.toast.error(error.message);
}
});
}
I am using Angular and angularfire. The code above is not called directly from a component, but from a service. That service though is called from my LoginComponent.
Edit. My imports are:
import { Injectable, NgZone } from '#angular/core';
import { AngularFireAuth } from '#angular/fire/compat/auth';
import {
AngularFirestore,
} from '#angular/fire/compat/firestore';
import { Router } from '#angular/router';
import { child, get, getDatabase, ref, set } from "firebase/database";
import { HotToastService } from '#ngneat/hot-toast';
import firebase from "firebase/compat/app";
import { getAuth, getMultiFactorResolver, GoogleAuthProvider, PhoneAuthProvider, PhoneMultiFactorGenerator, RecaptchaVerifier, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth';
As we talked in the comments for this to work you need an empty div with the passed id, like:
<div id="recaptcha-container-id"></div>
I'm using Vue 3.2 <script setup>, If I try to acess Pinia's store inside an API Service It throws the following error;
Uncaught ReferenceError: Cannot access 'store' before initialization at api.js?:9:1 (anonymous) # api.js?9:9
src/services/api.js:
import axios from 'axios';
import store from '../stores/index';
// eslint-disable-next-line no-undef
const api = axios.create({ baseURL: import.meta.env.VITE_APP_API_URL });
if (store) {
const { token } = store;
if (token) {
api.defaults.headers.Authorization = `Bearer ${token}`;
}
}
console.log(api);
export default api;
src/stores/index.ts:
import { defineStore } from 'pinia'
import Project from '../models/Project';
import { grantAuthSshd, revokeAuth, parseJwt } from '../services/auth';
const initialUser = JSON.parse(sessionStorage.getItem('Orcamento:token') || '{}');
const useProject = defineStore('project-store', {
state: () => ({
loading: false as boolean,
}),
actions: {
loadingDataTable(status: ((status: boolean) => void) & boolean) {
this.loadingDataTable = status;
},
}
});
I tried to use Pinia's interceptors but the error persists:
import axios from 'axios';
import useProject from '../stores/index';
const api = axios.create({ baseURL: import.meta.env.VITE_APP_API_URL });
// use interceptors
api.interceptors.request.use(
(config) => {
const { token } = store;
if ({token}) {
api.config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
const store = useProject();
export default api;
The problem is that there is indirect circular dependency between services/api.js and stores/index.ts modules, to the point they cannot be evaluated correctly.
useProject() returns a singleton, one of reasons why a store is wrapped with a function is that this prevents it from being accessed too soon. Pinia stores are supposed to be accessed only after Pinia is initialized, otherwise this would require to evaluate the modules that depend on it in a specific order that isn't easy to achieve.
In this case useProject is supposed to be used in-place, not on module evaluation:
api.interceptors.request.use(
(config) => {
const store = useProject();
const { token } = store;
...
Due to how ES modules work, this allows to resolve circular dependency.
A way to avoid circular dependency is to move this code from services/api.js to another module that stores/index.ts doesn't depend on, e.g. entry point.
I have a Vue.js application and on the /callback route I am trying to have it do a few things. So far I am not having any luck with it because I am seeing things run async. I understand that it is normally how Vue/Javascript works however I am trying to force it to not be async.
The main issue I am having is sometimes the this.$store... are running before the items are set. This is an issue because of how things run on other Vuex actions. Mainly the getCompany one requires the loadToken one to complete as it is pulling the values from the local storage which is being set above.
I don't want to change this and how it works because of how the Vue router is set up to pull the token from local storage on each page reload. This token is used to connect to the backend so it needs to be pulled from local storage each reload as I don't want a user to log in just because they reload the page.
Code:
created() {
setTimeout(() => {
localStorage.setItem('token', this.$auth.token)
localStorage.setItem('user_data', JSON.stringify(this.$auth.user))
// Load company data
this.$store.dispatch('loadToken')
this.$store.dispatch('getCompany')
if(this.$auth == null || this.$auth.id_token['https://hello.io/account_signup_type/is_new']) {
this.$router.push('/setup')
} else {
// Load user data from Auth0
// Go to chat page
this.$router.push('/chat')
}
}, 500)
}
edit main.js code
import { Auth0Plugin } from '#/auth/auth0-plugin';
// Install the authentication plugin
Vue.use(Auth0Plugin, {
domain,
clientId,
audience,
onRedirectCallback: (appState) => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname,
);
},
});
auth0-plugin
/**
* External Modules
*/
import Vue from 'vue';
import createAuth0Client from '#auth0/auth0-spa-js';
/**
* Vue.js Instance Definition
*/
let instance;
export const getInstance = () => instance;
/**
* Vue.js Instance Initialization
*/
export const useAuth0 = ({
onRedirectCallback = () =>
window.history.replaceState({}, document.title, window.location.pathname),
redirectUri = `${window.location.origin}/callback`,
...pluginOptions
}) => {
if (instance) return instance;
instance = new Vue({
data() {
return {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: {},
error: null,
token: null,
id_token: null
};
},
methods: {
async handleRedirectCallback() {
this.isLoading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
} catch (error) {
this.error = error;
} finally {
this.isLoading = false;
}
},
loginWithRedirect(options) {
return this.auth0Client.loginWithRedirect(options);
},
logout(options) {
return this.auth0Client.logout(options);
},
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
}
},
async created() {
this.auth0Client = await createAuth0Client({
...pluginOptions,
// responseType: 'id_token',
domain: pluginOptions.domain,
client_id: pluginOptions.clientId,
audience: pluginOptions.audience,
redirect_uri: redirectUri,
});
try {
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
const { appState } = await this.auth0Client.handleRedirectCallback();
onRedirectCallback(appState);
}
} catch (error) {
this.error = error;
} finally {
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.$auth.getTokenSilently().then(token => this.token = token)
this.$auth.getIdTokenClaims().then(id_token => this.id_token = id_token)
this.isLoading = false;
}
},
});
return instance;
};
/**
* Vue.js Plugin Definition
*/
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
edit - updated router.beforeEach
router.beforeEach(async (to, from, next) => {
const auth = getInstance()
if(to.path == '/callback' && auth != null) {
console.log('Callback')
console.log(`Token: ${auth.token}`)
console.log(`User: ${JSON.stringify(auth.user)}`)
localStorage.setItem('token', auth.token)
localStorage.setItem('user_data', JSON.stringify(auth.user))
await store.dispatch('loadToken')
await store.dispatch('getCompany')
return next()
}
if(to.path != '/login' && to.path != '/setup') {
await store.dispatch('loadToken')
await store.dispatch('getCompany')
.then(() => {
return next()
})
} else {
return next()
}
})
edit - adding guide that I followed from Auth0 to get the code I have now - mostly
https://auth0.com/blog/complete-guide-to-vue-user-authentication/
The problem is that there is race condition because dispatch calls return promises that weren't chained before accessing the result of their work.
A good practice is to chain every promise, unless proven other wise.
The code that created contains actually belongs to the router in general because authentication logic is application-wide.
It's unnecessary to access global dependencies on this component instance. This is done for historical reasons because Vue originally was used in non-modular environment. In order to use outside components, global dependencies such as store need to be explicitly imported. In case this cannot be done, this needs to be fixed.
In this case auth instance is available through getInstance. In case the authentication shouldn't be done on each navigation, this needs to be done on condition, e.g.:
import { getInstance } from '.../auth';
import store from '.../store';
...
router.beforeEach(async (to, from, next) => {
const auth = getInstance();
if (...) {
...
await store.dispatch('loadToken')
await store.dispatch('getCompany')
...
next('/setup')
...
} else {
next()
}
})
getInstance doesn't serve a good purpose because it just exposes a variable. Instead, instance could be exported and imported directly, the behaviour would be the same due to how ES modules work.
Also global store already holds application logic and commonly used to handle authentication, including local storage operations.
I use keycloak-js to authenticate my vue.js like this (taken from there: https://www.keycloak.org/securing-apps/vue), it´s my main.js:
import { store } from "./store/store";
let keycloak = Keycloak(initOptions);
keycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {
if (!auth) {
window.location.reload();
} else {
console.log("Authenticated");
new Vue({
vuetify, router, store,
render: h => h(App, { props: { keycloak: keycloak } })
}).$mount('#app')
}
const decoded = VueJwtDecode.decode(keycloak.token)
const roles = decoded.realm_access.roles
store.commit("storeRoles", roles)
//Token Refresh
setInterval(() => {
if(store.state.userData.logged_out) { ### done in vuex
keycloak.logout()
} else {
keycloak.updateToken(70).then((refreshed) => {
if (refreshed) {
console.log('Token refreshed' + refreshed);
} else {
console.log('Token not refreshed, valid for '
+ Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');
}
}).catch(() => {
console.log('Failed to refresh token');
});
}
}, 2000)
}).catch(() => {
console.log("Authenticated Failed");
});
I currently commit a mutation in my store to handle this via a button.
if(store.state.userData.logged_out) { ### done in vuex
keycloak.logout()
}
## Handles by a button in my view
this.$store.commit("logout", true)
This seems to be a bit hacky and I have to wait 2 seconds before my user actually gets logged out. Is there a way to directly access this keycloak instance from a component?
I solved it by adding the keycloak instance to the Vue.prototype (thanks #augstin gorni for the hint)
https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
let keycloak = Keycloak(initOptions);
Vue.prototype.$keycloak = keycloak
Now it´s globally accessible
logout() {
this.$keycloak.logout()
}
}
In Vue 3 you can use like below;
app.config.globalProperties.$keycloak = keycloak;
I have used create-react-app to scaffold the initial react application.
My DashBoard component:
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import $ from 'jquery';
import 'signalr';
class Dashboard extends Component {
constructor(props) {
super(props);
var connection = $.hubConnection('http://[address]:[port]');
var proxy = connection.createHubProxy('[hubname]');
// atempt connection, and handle errors
connection.start()
.done(function(){ console.log('Now connected, connection ID=' + connection.id); })
.fail(function(){ console.log('Could not connect'); });
}
render() {
return (...);
}
}
export default Dashboard;
Now I get the below error from SignalR saying jQuery is not added, but I have imported it in the line above:
Error: jQuery was not found. Please ensure jQuery is referenced before
the SignalR client JavaScript file.
If I comment out import "signalr"; jQuery gets loaded correctly and i can access the $ inside the module. Why does this happen?
This is how we do it now (year 2020) with the new package #microsoft/signalr.
We use Redux, but you don't have to use Redux to be able to utilize this method.
If you are using #microsoft/signalr package instead of #aspnet/signalr, then this is how you can set it up. This is our working code in prod:
import {
JsonHubProtocol,
HubConnectionState,
HubConnectionBuilder,
LogLevel
} from '#microsoft/signalr';
const isDev = process.env.NODE_ENV === 'development';
const startSignalRConnection = async connection => {
try {
await connection.start();
console.assert(connection.state === HubConnectionState.Connected);
console.log('SignalR connection established');
} catch (err) {
console.assert(connection.state === HubConnectionState.Disconnected);
console.error('SignalR Connection Error: ', err);
setTimeout(() => startSignalRConnection(connection), 5000);
}
};
// Set up a SignalR connection to the specified hub URL, and actionEventMap.
// actionEventMap should be an object mapping event names, to eventHandlers that will
// be dispatched with the message body.
export const setupSignalRConnection = (connectionHub, actionEventMap = {}, getAccessToken) => (dispatch, getState) => {
const options = {
logMessageContent: isDev,
logger: isDev ? LogLevel.Warning : LogLevel.Error,
accessTokenFactory: () => getAccessToken(getState())
};
// create the connection instance
// withAutomaticReconnect will automatically try to reconnect
// and generate a new socket connection if needed
const connection = new HubConnectionBuilder()
.withUrl(connectionHub, options)
.withAutomaticReconnect()
.withHubProtocol(new JsonHubProtocol())
.configureLogging(LogLevel.Information)
.build();
// Note: to keep the connection open the serverTimeout should be
// larger than the KeepAlive value that is set on the server
// keepAliveIntervalInMilliseconds default is 15000 and we are using default
// serverTimeoutInMilliseconds default is 30000 and we are using 60000 set below
connection.serverTimeoutInMilliseconds = 60000;
// re-establish the connection if connection dropped
connection.onclose(error => {
console.assert(connection.state === HubConnectionState.Disconnected);
console.log('Connection closed due to error. Try refreshing this page to restart the connection', error);
});
connection.onreconnecting(error => {
console.assert(connection.state === HubConnectionState.Reconnecting);
console.log('Connection lost due to error. Reconnecting.', error);
});
connection.onreconnected(connectionId => {
console.assert(connection.state === HubConnectionState.Connected);
console.log('Connection reestablished. Connected with connectionId', connectionId);
});
startSignalRConnection(connection);
connection.on('OnEvent', res => {
const eventHandler = actionEventMap[res.eventType];
eventHandler && dispatch(eventHandler(res));
});
return connection;
};
Then you would call like the following. Please note that this a pseudo code. You may have to call it differently depending on your project setup.
import { setupSignalRConnection } from 'fileAbove.js';
const connectionHub = '/hub/service/url/events';
export const setupEventsHub = setupSignalRConnection(connectionHub, {
onMessageEvent: someMethod
}, getAccessToken);
export default () => dispatch => {
dispatch(setupEventsHub); // dispatch is coming from Redux
};
Let me know if it helped by up-voting. Thank you
UPDATE: Please note that if you are using Redux in your ReactJS app, the solution below is not necessarily the best solution. It is better to implement signalR as a middleware. You can find the best answer here.
If you are not using Redux, or you still want to implement it in a React component, then read on:
For people that are using the latest version of signalR (core v2.1), since jQuery is not a dependency of signalR any more, you can import it like:
import * as signalR from '#aspnet/signalr';
NOTE: there is now a newer version of signalr available (#microsoft/signalr) that requires a different setup. This solution only works with #aspnet/signalr. (UPDATE June 2020)
And then use it like:
signalR.HubConnectionBuilder()
Here is an example:
import React, { PureComponent } from 'react';
import { string } from 'prop-types';
import * as signalR from '#aspnet/signalr';
class SignalR extends PureComponent {
constructor (props) {
super(props);
this.connection = null;
this.onNotifReceived = this.onNotifReceived.bind(this);
}
componentDidMount () {
const protocol = new signalR.JsonHubProtocol();
const transport = signalR.HttpTransportType.WebSockets;
const options = {
transport,
logMessageContent: true,
logger: signalR.LogLevel.Trace,
accessTokenFactory: () => this.props.accessToken,
};
// create the connection instance
this.connection = new signalR.HubConnectionBuilder()
.withUrl(this.props.connectionHub, options)
.withHubProtocol(protocol)
.build();
this.connection.on('DatabaseOperation', this.onNotifReceived);
this.connection.on('DownloadSession', this.onNotifReceived);
this.connection.on('UploadSession', this.onNotifReceived);
this.connection.start()
.then(() => console.info('SignalR Connected'))
.catch(err => console.error('SignalR Connection Error: ', err));
}
componentWillUnmount () {
this.connection.stop();
}
onNotifReceived (res) {
console.info('Yayyyyy, I just received a notification!!!', res);
}
render () {
return <span />;
};
};
SignalR.propTypes = {
connectionHub: string.isRequired,
accessToken: string.isRequired
};
export default SignalR;
UPDATE: in 2020, you can use "withAutomaticReconnect()":
const connection = new HubConnectionBuilder()
.withUrl(connectionHub, options)
.withAutomaticReconnect()
.withHubProtocol(new JsonHubProtocol())
.configureLogging(LogLevel.Information)
.build();
What I figured out Signalr has dependency on jQuery. For some reason import $ from 'jquery' doesn't set window.jQuery. That's why need to do it explicitly.
I solved the issue this way:
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import $ from 'jquery';
window.jQuery = $;
require('signalr');
class Dashboard extends Component {
// .....
}
export default Dashboard;
Check out SignalR no jQuery
npm i -D signalr-no-jquery
import { hubConnection } from 'signalr-no-jquery';
const connection = hubConnection('http://[address]:[port]', options);
const hubProxy = connection.createHubProxy('hubNameString');
// set up event listeners i.e. for incoming "message" event
hubProxy.on('message', function(message) {
console.log(message);
});
// connect
connection.start({ jsonp: true })
.done(function(){ console.log('Now connected, connection ID=' + connection.id); })
.fail(function(){ console.log('Could not connect'); });
https://www.npmjs.com/package/signalr-no-jquery