so what I am currently working on is using vue with socketIO. So I want a component that updates its data when the websocket emits something.
I looked at the following tutorial: https://medium.com/#michaelmangial1/getting-started-with-vue-js-socket-io-8d385ffb9782.
The connection to the websocket works fine however I don't know how I can change a data variable of the component or call a method of the component from the websocket function as I get the error that e.g. this.getAllLayouts is not a function.
In the tutorial they just set app.title = data.title; in socket.on() as the Vue instance can be addresses as app.
But now I am confused. I wanted to use this in an component which I defined as a single-file component. So I can't use this app reference. But how can I reference the component in that case?
Below you can see the most important parts of this component. I defined the socket connection in the created() of the component
<script>
import axios from 'axios';
// eslint-disable-next-line
import io from 'socket.io-client';
import FirstPage from './FirstPage.vue';
import DNDAssign from './DNDAssign.vue';
export default {
name: 'ControlCenter',
data() {
return {
devices: [{ ip: 'yet unknown' }], // placeholder so line 12 does not throw error before actual device info fetched
thisDeviceIndex: 0,
currentLayoutIndex: 0,
layouts: [],
};
},
components: {
DNDAssign,
FirstPage,
},
methods: {
getAllLayouts() {
const path = 'http://192.168.0.38:5000/layouts';
axios.get(path)
.then((response) => {
console.log(response);
this.layouts = response.data.layouts;
this.devices = response.data.devices;
this.thisDeviceIndex = response.data.your_device_index;
console.log(this.layouts);
})
.catch((error) => {
// eslint-disable-next-line
console.log(error);
});
},
},
created() {
// inital fetching of layouts
console.log('fetching layouts from backend');
this.getAllLayouts();
// test websocket connection
const socket = io.connect('http://192.168.0.38:5000');
// eslint-disable-next-line
socket.on('connect', function () {
console.error('connected to webSocket');
socket.emit('my event', { data: 'I\'m connected!' });
});
// eslint-disable-next-line
socket.on('my response', function(data){
console.log('got response');
console.log(data.data);
});
// eslint-disable-next-line
socket.on('update_on_layouts', function(data){
this.getAllLayouts(); //this does not work? How can I make it work or do something like
// this.layouts = data;
});
},
};
</script>
Issue is that this inside that fn is not binded to Vue, you need to lexically bind it
Use arrow method to bind this to method
socket.on('update_on_layouts', data => {
this.getAllLayouts(); //this does not work? How can I make it work or do something like
// this.layouts = data;
});
Related
I am making a chat app using Firebase and RN. In my firebase code I have a function like this:
//all values are declared before, db is from firebase config which i do not wish to share
import "firebase";
async function getPublic(dba = db) {
const messages = onSnapshot(doc( /*collection name ->*/"public", dba), db => db.docs())
return messages;
}
Is there a way to update the returned value or something similar to that?
Instead of naming your function getPublic, consider instead usePublic. Or even better, generalize it so you can use different paths.
But first, we need to look at the definition of onSnapshot() (a CollectionReference extends from Query):
export declare function onSnapshot<T>(query: Query<T>, observer: {
next?: (snapshot: QuerySnapshot<T>) => void;
error?: (error: FirestoreError) => void;
complete?: () => void;
}): Unsubscribe;
As you can see here, the messages aren't returned from this function, but an Unsubscribe function is (a () => void). So to update a messages array, you'll need to use useState and because you are using realtime listeners, you should use useEffect to manage the listener lifecycle. You also should handle the intermediate states such as loading, errored and fetched data. This results in:
import { useEffect, useState } from 'react';
import { getFirestore, collection, onSnapshot } from "firebase/firestore";
function useMessageFeed(feed = "public", firestore = getFirestore()) { // use default firestore instance unless told otherwise
// set up somewhere to store the data
const [ messagesInfo, setMessagesInfo ] = useState(/* default messagesInfo: */ {
status: "loading",
messages: null,
error: null
});
// attach and manage the listener
useEffect(() => {
const unsubscribe = onSnapshot( // unsubscribe is a () => void
collection(/* firestore instance: */ firestore, /* collection path: */ feed),
{
next: querySnapshot => setMessagesInfo({
status: "loaded",
messages: querySnapshot.docs(), // consider querySnapshot.docs().map(doc => ({ id: doc.id, ...doc.data() }))
error: null
}),
error: err => setMessagesInfo({
status: "error",
messages: null,
error: err
})
}
);
return unsubscribe;
}, [firestore, feed]); // <-- if these change, destroy and recreate the listener
return messagesInfo; // return the data to the caller
}
Elsewhere in your code, you would use it like this:
const SomeComponent = (props) => {
const { status, messages, error: messagesError } = useMessageFeed("public");
switch (status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {messagesError.message}
</div>
);
}
// render messages
return (
/* ... */
);
}
I'm a beginner in vue and created my first application. I use vuex and I have a plugin to manage a webSocket to the server. Everything works fine as long as I only dealt with messages sent from the server to the browser.
I now would like to add a function to send messages through the socket if connected, but I'm not able to access the exported function. I'm also a beginner in Javascript programming.
Here is the plugin code:
var store = null;
var ws = null;
function startWebsocket() {
ws = new WebSocket(process.env.VUE_APP_WEBSOCKET_URL)
ws.onmessage = function (event) {
console.log("webSocket: on message: ", event.data);
store.dispatch('remoteMessage', event.data);
}
ws.onopen = function (event) {
console.log("webSocket: on open: ", event)
store.dispatch('connectionOpened');
}
ws.onclose = function (event) {
console.log("webSocket: on close: ", event)
store.dispatch('connectionClosed');
ws = null
setTimeout(startWebsocket, 5000)
}
ws.onerror = function (event) {
console.log("webSocket: on error: ", event)
}
}
export default function createWebSocketPlugin() {
return store_param => {
store = store_param;
startWebsocket();
};
}
I would like to add the following function to the plugin so that I can call it from a vuex action function.
export function sendWebSocketMsg(msg) {
if (ws) {
ws.sendMsg(msg)
}
}
In the vuex index.js file I have this:
. . .
import webSocket from '../plugins/webSocket'
. . .
export default new Vuex.Store({
. . .
actions: {
connectionOpened({ commit }) {
commit('SET_CONNECTION', true);
},
connectionClosed({ commit }) {
commit('SET_CONNECTION', false);
},
connectionError({ commit }, error) {
commit('SET_ERROR', error);
},
remoteMessage({commit}, message) {
commit('SET_MESSAGE', message);
},
pause() {
sendWebSocketMsg('{"pause":true}')
},
play() {
sendWebSocketMsg('{"pause":false}')
}
}
}
The webSocket works well and reconnects automatically.
The only thing that I'm missing is the ability to send a webSocket message.
How do I have to modify the webSocket plugin ?
I answer my question since I found the solution. It is partly given in the tutorial I followed.
I wasn't aware of it, but the plugin is a vuex plugin.
The solution is to subscribe to a vuex method. I added the empty method SEND_MESSAGE to the vuex mutations.
mutations: {
SET_ERROR(state, errStr) {
state.error = errStr;
},
SET_CONNECTION(state, status) {
state.connected = status;
},
SET_MESSAGE(state, message) {
let msg = JSON.parse(message);
. . .
},
SEND_MESSAGE() {
},
},
I also added the application specific actions:
pause({commit}) {
commit('SEND_MESSAGE', '{"pause":true}');
},
play({commit}) {
commit('SEND_MESSAGE', '{"pause":false}');
},
I call the store actions from my components like this:
methods: {
pause() {
this.$store.dispatch("pause");
},
play() {
this.$store.dispatch("play");
}
},
The only change left to do is in the plugin. I subscribe a method to call to the SEND_MESSAGE mutation. This is how it is done:
export default function createWebSocketPlugin() {
return store_param => {
store = store_param;
startWebsocket();
store.subscribe((mutation, state) => {
if (state.connected && mutation.type === 'SEND_MESSAGE' && ws) {
console.log("webSocket send "+mutation.payload);
ws.send(mutation.payload);
}
});
};
}
I added the store.subscribe instruction. We only perform the operation when the mutation is of the right type and the web socket is connected.
ws variable is local to the module it was defined, this requires to modify plugin module in order for a function to access ws, e.g.:
export function sendWebSocketMsg(msg) {
if (ws) {
ws.sendMsg(msg)
}
}
export default function createWebSocketPlugin() {...}
Then named export can be imported in module where it's used:
import webSocket, {sendWebSocketMsg} from '../plugins/webSocket'
I'm attempting to add an Axios plugin to Nuxt as described here, but it doesn't seem to work.
This is my plugins/axios.js file...
export default function({ $axios }) {
console.log('Im in the axios plugin')
$axios.defaults.baseURL = `https://localhost:5001/api`
$axios.defaults.headers = {
Accept: 'application/json',
'Content-Type': 'application/json'
}
$axios.onRequest((config) => {
console.log('Making request to ' + config.url)
})
}
This is my nuxt.config.js
plugins: ['~/plugins/axios'],
modules: ['#nuxtjs/axios']
And this is where I use Axios in a file called services/BookService.js:
import axios from 'axios'
export default {
getBooks() {
return axios.get('/Home')
},
getBooksFiltered(payload) {
return axios.post('/Home/Filters', payload)
}
}
I get the console.log('Im in the axios plugin') from within my plugin, but nothing else. $axios.onRequest doesn't appear to run, and the baseURL doesn't appear to be set correctly when getBooksFiltered is triggered. I get a 404 when it tried to hit the address http://localhost:3000/Home/Filters. As described in my plugin, the address should be https://localhost:5001/api/Home/Filters
I've also tried the following in my nuxt.config.js, but it doesn't work:
axios: {
baseURL: 'https://localhost:5001/api'
}
Any ideas?
Edit
I've modified my services/BookService.js based on the suggestion below to the following...
export default {
getBooks(axios) {
console.log('Im in getBooks')
return axios.get('/Home')
}
}
My action request that makes my api call is the following....
import BookService from '~/services/BookService.js'
export const fetchBooks = (context) => {
console.log('Im in fetchBooks action')
return BookService.getBooks(this.$axios)
.then((response) => {
context.commit('SET_BOOKS', response.data.booksList)
})
.catch((error) => {
console.log(error)
})
}
And my method in my component that calls the actions...
async fetch({ store, error }) {
try {
console.log('Im in index -> fetch')
await store.dispatch('fetchBooks')
} catch (e) {
error({
statusCode: 503,
message: 'Unable to fetch books at this time'
})
}
}
I'm aware that I may be mixing async/await with promises incorrectly but I don't believe it's the cause of this issue.
Console returns the following...
My network tab contains a single request to http://localhost:3000/ which seems incorrect. It should be https://localhost:5001/api/Home based on the plugin and the address specified in the action. It is also never entering $axios.onRequest
The axios-module sets up an Axios instance on the Nuxt app instance. When you import Axios from axios, and use it directly, you're not using the previously setup Axios instance.
To fix the issue, you could either reference the preconfigured Axios instance from window.$nuxt.$axios (only in the browser), or setup your service to take an Axios instance as a parameter:
// services/BookService.js
export default axios => ({
getBooks() {
return axios.get('/Home')
},
getBooksFiltered(payload) {
return axios.post('/Home/Filters', payload)
}
})
// store.js
import BookService from '~/services/BookService.js'
export default {
actions: {
async getBooks({ commit }) {
const books = await new BookService(this.$axios).getBooks()
commit('SET_BOOKS', books)
}
}
}
Another solution from nuxt-community/axios-module #28:
~/plugins/axios-port.js
import { setClient } from '~/services/apiClient'
export default ({ app, store }) => {
setClient(app.$axios)
}
~/services/apiClient.js
let client
export function setClient (newclient) {
client = newclient
}
// Request helpers
const reqMethods = [
'request', 'delete', 'get', 'head', 'options', // url, config
'post', 'put', 'patch' // url, data, config
]
let service = {}
reqMethods.forEach((method) => {
service[method] = function () {
if (!client) throw new Error('apiClient not installed')
return client[method].apply(null, arguments)
}
})
export default service
Use:
import apiClient from '~/services/apiClient'
export default {
async current () {
return apiClient.get('...')
}
}
In my case I exported a customized axios instance as the doc suggested in my axios.js
export default function ({ $axios }, inject) {
const api = $axios.create({
baseURL:'/api'
})
// Inject to context as $api
inject('api', api)
}
Then use this.$api.get or this.$api.post in your getBook service
The above one works for me
As I have just tested, in each request we should use $axios.
Example: this.$axios.get('....'), or in another context this.$nuxt.$axios.get('...');
Because axios extension use with the app context instance, if we import, it will create a new instance which plugin cannot extend.
I have put test code on stackblitz: here
It seems you need to yarn add #nuxtjs/axios or npm install #nuxtjs/axios like the setup instruction here before it can work: https://axios.nuxtjs.org/setup
I haven't experienced with nuxt yet but I don't think by adding some line of code into some js file without actually installing will make the package available into your repo.
Here is a part of my React component:
import React from 'react';
import { Client } from '#stomp/stompjs';
class Balance extends React.Component {
componentDidMount() {
const client = new Client({
brokerURL: 'ws://localhost:8080/stomp',
debug: (str) => {
console.log(str);
},
});
client.onConnect(() => {
console.log('onConnect');
client.subscribe('/topic/balance', message => {
console.log(message);
})
});
client.activate();
}
...
It looks like connection was established according to the debug output to browser's console:
Opening Web Socket...
Web Socket Opened...
>>> CONNECT
accept-version:1.0,1.1,1.2
heart-beat:10000,10000
Received data
<<< CONNECTED
heart-beat:0,0
version:1.2
content-length:0
connected to server undefined
However, I don't see a message 'onConnect' in console, which means client.onConnect was never fired.
Therefore I can't subscribe to a topic.
What could be a problem here?
UPDATE:
According to author it was a mix up in syntax of the library.
The corrected code from my question look as the following:
import React from 'react';
import { Client } from '#stomp/stompjs';
class Balance extends React.Component {
componentDidMount() {
// The compat mode syntax is totally different, converting to v5 syntax
// Client is imported from '#stomp/stompjs'
this.client = new Client();
this.client.configure({
brokerURL: 'ws://localhost:8080/stomp',
onConnect: () => {
console.log('onConnect');
client.subscribe('/topic/balance', message => {
console.log(message);
})
},
// Helps during debugging, remove in production
debug: (str) => {
console.log(new Date(), str);
}
});
this.client.activate();
}
...
I created a full working example in my repo.
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