I'm working with WebSocket and having an issue with a function showing incorrect data, All my code used to work with a Class-based component, I'm only trying to convert a class component that actually calls the connect method of Websocket to a functional-based component. The Websocket is class-based for instance.
So I have a WebSocket which on socket New Message sets callbacks.
socketNewMessage(data) {
console.log(data);
const parsedData = JSON.parse(data);
const command = parsedData.command;
if (command === "userChatGroups") {
this.callback[command](parsedData.chatGRoups);
}
if (command === "new_userChatGroups") {
this.callback[command](parsedData.chatGRoup);
}
}
and here are the callbacks defined -
addCallbacks(userChatGroups, newUserChatGroup) {
this.callback["userChatGroups"] = userChatGroups;
this.callback["new_userChatGroups"] = newUserChat;
}
and then websocket finally return -
const WebSocketInstance = Websocketservice.getInstance();
export default WebSocketInstance;
Now the class-based component which I'm trying to convert to functional based -
let's call this component Chats-
this calls the connect method and a Websocket instance is returned.
useEffect(() => {
if (loggedInUserDetail) {
WebSocketInstance.connect(loggedInUserDetail[0].id);
}
}, [loggedInUserDetail]);
I have a helper function which checks the status of websocket -
function waitForSocketConnection(callback) {
setTimeout(() => {
if (WebSocketInstance.state() === 1) {
console.log("connection is secure");
callback();
return;
} else {
console.log("waiting for connection");
waitForSocketConnection(callback);
}
}, 10);
}
and I check the status and map my callbacks - here is the problem-
useEffect(() => {
waitForSocketConnection(() => {
WebSocketInstance.addCallbacks(
setChatGroups,
addNewChatGroup
);
});
}, [loggedInUserDetail]);
I have a state to manage CHatGroups -
const [groups, setGroups] = useState([]);
the setChatGroups (which initially loads all the groups the users are associated with works fine and sets the state of chatsGroups) as -
const setChatGroups = useCallback((userChatGroups) => {
setGroups(userChatGroups); //whatever data it recieved from the websocket.
}, []);
but the function addNewChatGroup always shows groups value as an empty array [] (it was updated earlier with setChatGroups). If I manually check the value of groups it is an array of n length, but in addNewChatGroup function, it always shows an empty array with the initial value [].
const addNewChatGroup = useCallback(
(newCHatGroup) => {
console.log(groups); **error -> this is always empty array**
// here I have to update. add the received value with the previous
},
[groups] // callbacks used hoping this will bind
);
In the class-based component I used to set callbacks on the constructor and used to bind with this, but I'm not able to do it here, can anyone help what I'm missing?
I'm sure it is a binding issue. maybe. May I know the reason for this binding failure?
Well if I understood correctly your problem, it could be link to multiple things.
The problem actually is I don't have a clear view on all your components, maybe you can try to paste a sandbox link or something like that with a "simple structure".
I tried to reproduced a typescript version, I don't know if it could help:
class MyWebSocket {
private static _instance: MyWebSocket;
public callbacks: any = {};
public connected: boolean = false;
public socketNewMessage(data: any): void {
const parsedData = JSON.parse(data);
console.log('new message received:', parsedData);
const command = parsedData.command;
if (command === "new_userChatGroups") {
this.callbacks[command](parsedData.newGroupAdded);
}
}
public addCallbacks(elements: {command: string, func: Function}[]) {
console.log('adding callbacks...', elements);
elements.forEach(element => {
this.callbacks[element.command] = element.func;
});
}
public connect(): void {
setTimeout(() => this.connected = true, 1100);
}
public static getInstance(): MyWebSocket {
return this._instance || (this._instance = new MyWebSocket());
}
}
class SocketUtils {
static waitForSocketConnection(callback: any): void {
const waitingInterval = setInterval(() => {
if (MyWebSocket.getInstance().connected) {
console.log('socket is connected! processing callback...');
clearInterval(waitingInterval);
callback();
return;
} else {
console.log('socket is not connected after 1sec, waiting...');
}
}, 1000);
}
}
class Chat {
groups: string[] = ['group of JStw'];
new_userChatGroups(group: string) {
this.groups.push(group);
}
}
class Main {
constructor() {
const myChat = new Chat();
MyWebSocket.getInstance().connect();
// waiting connections.
SocketUtils.waitForSocketConnection(() => {
console.log('waitForSocketConnection is triggered, adding callbacks...');
// adding callbacks
MyWebSocket.getInstance().addCallbacks([{command: 'new_userChatGroups', func: myChat.new_userChatGroups.bind(myChat)}]);
});
// Waiting 5min to dispatch an message
setTimeout(() => {
// testing eventing after getting connection
MyWebSocket.getInstance().socketNewMessage(JSON.stringify({command: 'new_userChatGroups', newGroupAdded: 'group of Ranu Vijay'}));
}, 5000);
setTimeout(() => {
console.log('program finished, results of chat groups:', myChat.groups);
}, 10000);
}
}
new Main();
Output:
I'm more specialized on functional component by using react so without a link to investigate all your code, it will be complex to help.
I don't know if you are using a library, but there is this one: https://www.npmjs.com/package/react-use-websocket which seems to be really ultra simple to use without managing socket connection/disconnection.
For me if I had to implement it, I would say:
Component ChatGroup which is using websocket hook const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl); and contains the state groups, setGroups.
Component Chat which can use the sendMessage from props of ChatGroup component and call it from this component if a join a group.
Then your "parent" component is managing the state and is controlling the data.
Related
I'm trying to send API calls using the state of one of my hooks as a payload in the HTTP request. The issue I'm having is that the hook being asynchronous is causing irritating behaviour. The two issues I'm facing are:
The state is one step behind when sending the payload, so the HTTP request is going through with my initialised data which I don't want to send.
The component is not re-rendering with the updated state.
Some of my implementation code includes:
Function definition for get request that sets state to the role i'm using
function roleSelector(role_id: string) {
webHelpers.get('/api/worker/role/' + role_id, environment, 'api', token, (data: any) => {
setLocalRole(data);
});
}
Function that handles the changes in component which trigger the cycle and call the above function as well as send off the request after.
function handleAuxChange(aux_value: boolean, role_id: string) {
roleSelector(role_id);
localRole.skills = [];
localRole.allow_aux = !aux_value;
updateRole(localRole);
}
Function that sends off the HTTP POST request:
function updateRole(role: UserRole) {
let skills = [...role.skills];
role.details.forEach(s => {
skills.push({ skill_id: s.skill.id, level_id: s.level.id })
});
role.skills = skills;
webHelpers.post('/api/worker/role', environment, 'api', role, token, (data: any) => {
if (data.status === undefined) {
enqueueSnackbar('role successfully set to active!', { 'variant': 'success' });
}
else {
console.log(role);
enqueueSnackbar(`${data.status}: Unable to apply changes to the role`, { 'variant': 'error' })
}
});
}
Is there something in my implementation that i'm doing/not doing that's leading me to be a step behind in state? I've tried writing in a useEffect hook too with localRole as an entry in the default dependency array but this was no help for me either.
You could make roleSelector return the data. And the handleAuxChange can use the info directly.
eg.
function roleSelector(role_id: string, cb: (data:any) => void) {
return webHelpers.get('/api/worker/role/' + role_id, environment, 'api', token, (data: any) => {
setLocalRole(data);
cb(data);
});
}
function handleAuxChange(aux_value: boolean, role_id: string) {
roleSelector(role_id, localRole => {
localRole.skills = [];
localRole.allow_aux = !aux_value;
updateRole(localRole);
});
}
This is part of my code, what I want to do is this component at any time can receive a message on any of the conversations. Sending a message triggers a Socket event which triggers this code below, but I can't seem to get the "latest" conversations, as the useEffect only triggers when the component mounts (at that point my conversations array has zero length).
What I was thinking is that I should include "conversations" on the useEffect's dependency but that would create multiple websocket connection, one each time a Socket.io event is triggered because it does change the state. Is this the best solution? Thanks in advance!
const [conversations, setConversations] = useState<Array<Conversations>>([]);
useEffect(() => {
async function getConversations() {
try {
const { data } = await axios.get("/api/conversations/");
if (data.success) {
setConversations(data.details);
}
} catch (err) {}
}
getConversations();
socketInstance.on("connect", () => {
console.log("Connecting to Sockets...");
socketInstance.emit("authenticate", Cookies.get("token") || "");
});
socketInstance.on("ackAuth", ({ success }) => {
console.log(
success
? "Successfully connected to Sockets"
: "There has been an error connecting to Sockets"
);
});
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
(conv: Conversations) => {
return conv.conversationId === data.conversationId;
}
);
modifiedConversation.messages.push({
from: {
firstName: data.firstName,
lastName: data.lastName,
profilePhoto: data.profilePhoto,
userId: data.userId,
},
content: data.content,
timeStamp: data.timeStamp,
});
const updatedConversations = [
...conversations.filter(
(conv) => conv.conversationId !== data.conversationId
),
modifiedConversation,
];
setConversations(updatedConversations);
});
}, []);
While attaching and removing the socket listeners every time conversations changes is a possibility, a better option would be to use the callback form of the setters. The only time you reference the state, you proceed to update the state, luckily. You can change
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
to
socketInstance.on("newMessage", (data) => {
setConversations(conversations => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
You should also not mutate the state, since this is React. Instead of
modifiedConversation.messages.push({
do
const modifiedConversationWithNewMessage = {
...modifiedConversation,
messages: [
...modifiedConversation.messages,
{
from: {
// rest of the object to add
I apologize if this is unclear, it's late and I don't know how best to explain it.
I'm using an event emitter to pass data from a server response to a function inside of a separate class in another file, but when trying to use methods in those classes, the this keyword obviously doesn't work (because in this scenario, this refers to the server event emitter) - how would I reference a function within the class itself? I've provided code to help illustrate my point a bit better
ServiceClass.js
class StreamService {
/**
*
* #param {} database
* #param {Collection<Guild>} guilds
*/
constructor (database, guilds,) {
.....
twitchListener.on('live', this.sendLiveAlert) // fire test method when we get a notification
// if there are streamers to monitor, being monitoring
winston.info('Stream service initialized')
}
..............
async get (url, params = null, headers = this.defaultHeaders) {
// check oauth token
const expirationDate = this.token.expires_in || 0
if (expirationDate <= Date.now() || !this.token) await this.getAccessToken()
// build URL
const index = 0
let paramsString = ''
for (const [key, value] of params.entries()) {
if (index === 0) {
paramsString += `?${key}=${value}`
} else {
paramsString += `&${key}=${value}`
}
}
const res = await fetch(url + paramsString, { method: 'GET', headers: headers })
if (!res.ok) {
winston.error(`Error performing GET request to ${url}`)
return null
}
return await res.json()
}
async sendLiveAlert(streamTitle, streamURL, avatar, userName, gameId, viewerCount, thumbnail, startDateTime) {
// get game name first (no headers needed)
const params = new Map()
params.set('id', gameId)
const gameData = await this.get('https://api.twitch.tv/heliix/games', params, this.defaultHeaders)
if(gameData) {
// get webhook and send message to channel
const webhookClient = new WebhookClient('755641606555697305', 'OWZvI01kUUf4AAIR9uv2z4CxRse3Ik8b0LKOluaOYKmhE33h0ypMLT0JJm3laomlZ05o')
const embed = new MessageEmbed()
.setTitle(`${userName} just went live on Twitch!`)
.setURL(streamURL)
.setThumbnail(avatar)
.addFields(
{ name: 'Now Playing', value: gameData.data[0].name },
{ name: 'Stream Title', value: streamTitle }
)
.setImage(thumbnail)
}
webhookClient.send('Webhook test', embed)
}
}
Server.js
class TwitchWebhookListener extends EventEmitter {
......................
// Routes
server
.post((req, res) => {
console.log('Incoming POST request on /webhooks')
............................
const data = req.body.data[0]
if(!this.streamerLiveStatus.get(data.user_id) && data.type === 'live') {
// pass request body to bot for processing
this.emit(
'live',
data.title, // stream title
`https://twitch.tv/${data.user_name}`, // channel link
`https://avatar.glue-bot.xyz/twitch/${data.user_name}`, // streamer avatar
data.user_name,
data.game_id,
data.viewer_count,
data.thumbnail_url,
data.started_at // do we need this?
)
}
break
default:
res.send(`Unknown webhook for ${req.params.id}`)
break
}
} else {
console.log('The Signature did not match')
res.send('Ok')
}
} else {
console.log('It didn\'t seem to be a Twitch Hook')
res.send('Ok')
}
})
}
}
const listener = new TwitchWebhookListener()
listener.listen()
module.exports = listener
Within the sendLiveAlert method, I'm trying to call the get method of the StreamService class - but because it's called directly via the emitter within server.js, this refers specifically to the Server.js class - is there any way I can use StreamService.get()? I could obviously just rewrite the code inside the method itself, but that seems unnecessary when its right there?
Change this:
twitchListener.on('live', this.sendLiveAlert)
to this:
twitchListener.on('live', this.sendLiveAlert.bind(this))
Or, you could also do this:
twitchListener.on('live', (...args) => {
this.sendLiveAlert(...args);
});
With .bind() it creates a function wrapper that resets the proper value of this for you. In the case of the arrow function, it preserves the lexical value of this for you.
I've done a lot of searching and can't seem to find the answer to this - maybe I'm just not using the right terminology.
What I need to do is pass data from a WebSocket component, down to a child component. I'm already passing the WebSocket via props to the child so that it can use the send() function and send data to the socket. I need to also pass any received data via onmessage. Setting this up in the usual way inside the child doesn't work.
What I need to happen is when the data is received in the socket it gets sent to the child, with a function inside the child to then do something with it (send it via MIDI using the Web MIDI API)
Parent
class Socket extends Component {
constructor(props) {
super(props);
this.state = {
ws: null,
};
}
componentDidMount() {
this.connect();
}
connect = () => {
var ws = new WebSocket("ws://127.0.0.1:8000/ws/");
ws.onopen = () => {
this.setState({ ws: ws });
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
var midi = data["command"]; // Need to send this to the child somehow.
};
......
}
render() {
return <MIDIComponent websocket={this.state.ws} />;
}
}
EDIT: So I've managed to get the data from the parent to the child, and I've rendered it to the screen for testing. But I can't extract it inside the functions I need. I've tried combinations of using arrow functions, binding 'this' etc. I either can't access this or the midi ports either come back as undefined or null, the default value.
Child
class MIDIComponent extends Component {
constructor(props) {
super(props);
this.state = {
midiInput: null,
midiOutput: null,
};
}
componentDidMount() {
const that = this;
this.setupMIDI(that);
}
setupMIDI = (that) => {
navigator.requestMIDIAccess({ sysex: true }).then(onMIDISuccess);
function onMIDISuccess(midiAccess) {
that.setState({ midiInput: Array.from(midiAccess.inputs.values())[0] });
that.setState({ midiOutput: Array.from(midiAccess.outputs.values())[1] });
that.state.midiInput.onmidimessage = getMIDIMessage;
// storing the midi ports in the state like this, but it doesnt work.
}
function getMIDIMessage(msg) {
console.log(msg.data);
that.props.websocket.send(
JSON.stringify({ message: Array.from(msg.data), type: "config" })
);
}
};
sendMIDIMessage = (msg) => {
this.state.midiOutput.send(msg); // need to get to the midiOutput port here to send the data
};
render() {
return <div key={this.props.midi}>{this.props.midi}</div>; // Just using this to render the data to the screen for testing
}
}
I should probably mention that I will be eventually having two Child Components that will need to receive data from the Socket depending on the type of data received. At the moment I'd just like to get it set up using one. Any help would be greatly appreciated.
Simply save the received data in the state as well like this:
class Socket extends Component {
constructor(props) {
super(props);
this.state = {
ws: null,
midi: [] // Create an empty array so that the child always received something and not undefined
};
}
componentDidMount() {
this.connect();
}
connect = () => {
var ws = new WebSocket("ws://127.0.0.1:8000/ws/");
ws.onopen = () => {
this.setState({ ws: ws });
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
const midi = data["command"]; // Need to send this to the child somehow.
this.setState({
midi // Save the received data in the state
});
};
}
render() {
const {ws, midi} = this.state; // Extract the data from the state
return <MIDIComponent websocket={ws} midi={midi}/>; // Pass the data as a prop to the child
}
}
I wonder how can i get the response, if the WebSocketSubject is connected?
In the source code (https://github.com/ReactiveX/rxjs/blob/master/src/observable/dom/WebSocketSubject.ts) they have a openObserver member of type NextObserver<Event>
socket.onopen = (e: Event) => {
const openObserver = this.openObserver;
if (openObserver) {
openObserver.next(e);
}
.....
How can i connect to it, to get the event, that the socket was opened?
The openObserver parameter accepts NextObserver interface so you can use:
let socket = new WebSocketSubject({
url: 'ws://localhost:8081',
openObserver: {
next: value => {
console.log(value);
}
}
});
Or you can create your own class that extends NextObserver and pass its instance in parameters as { ..., openObserver: new MyObserver() } .