I want to send a real-time notification to the owner of the post when the post is liked. But, I don't really have an idea how to implement it into my React app and make it work. My notification function on the server side looks like this;
const Notification = require("../models/NotificationModel.js");
const { Server } = require("socket.io");
const io = new Server({
cors: "clientURL",
});
const sendNotification = async ({
sender,
receiver,
message,
project,
comment,
challenge,
}) => {
io.on("connection", (socket) => {
socket.on("sendNotification", ({sender, receiver, message}) => {
io.to(receiver.socketId).emit("getNotification", {
sender,
message
})
});
});
await Notification.create({
message,
sender,
receiver,
project,
comment,
challenge,
});
};
module.exports = sendNotification;
According to the OpenGPT client-side should look like this;
import React, { useEffect } from 'react';
import io from 'socket.io-client';
const socket = io('serverURL');
const MyComponent = () => {
useEffect(() => {
socket.on("getNotification", (data) => {
// Handle the notification with the data from the server
});
}, []);
return (
// Render your component
);
};
Could you please point out all the wrongs in this snippet and provide the correct way to make these actions in sequence;
user likes a post -> server sends a notification to the owner of the post
Related
I have a file called socket_io.js where I created a single instance of a socket io client in my react app as shown below:
socket_io.js
import EndPoints from './http/endpoints';
import io from "socket.io-client";
const socketUrl = EndPoints.SOCKET_BASE;
let socketOptions = { transports: ["websocket"] }
let socket;
if (!socket) {
socket = io(socketUrl, socketOptions);
socket.on('connect', () => {
console.log(`Connected to Server`);
})
socket.on('disconnect', () => {
console.log(`Disconnected from Server`);
})
}
export default socket;
Then I imported the above singleton in many react components as shown below.
MessagePage.js
import socket from '../socket_io.js';
let messageHandler=(data)=>{
}
useEffect(()=>{
socket.on('message',messageHandler); //This event no longer fires When the singleton socket io instance is reconnected
return ()=>{
socket.off('message');
}
},[]);
which works well but the issue I'm facing now is that when the singleton instance reconnects, the components referencing it are no longer receiving events from their respective handlers.
Possible causes of reconnection are when I manually restart the server
How can this be resolved?
I just solved this after working on it for a project of my own. My method involves two parts: creating the socket in a useEffect hook and then managing it using useRef for reconnection situations.
In Summary:
I think there are two issues. One is that the socket is being initialized as a singleton and not using a hook/context. I've read other reports of strangeness in this case, so I suggest switching to using context and creating your socket in a hook. Secondly, we have to manually store reconnection logic (although by generating the socket properly, it seems as though the actual event listeners are kept through reconnect).
export const SocketContext = createContext();
export const SocketContextProvider = ({ children }) => {
const [socket, setSocket] = useState();
const reconnectEmits = useRef([]);
// Here's your basic socket init.
useEffect(()=>{
const newSocket = io(url);
setSocket(newSocket);
return () => {
newSocket.close();
}
}, []);
// Code used to rejoin rooms, etc., on reconnect.
newSocket.io.on('reconnect', e => {
console.log("it took " + e + " tries to reconnect.");
for (let action of reconnectEmits.current) {
newSocket.emit(action.event, action.data);
}
})
// Here I also define a setListener and removeListener function, which determine which listeners a socket listens to. I don't have the code in front of me now, but it's pretty simple:
const addListener = (event, function) => {
// I use socket.off(event) right here to make sure I only have one listener per event, but you may not want this. If you don't use it you will need to make sure you use hooks to remove the event listeners that your components add to your socket when they are removed from the DOM.
socket.on(event, function);
}
// I implement an emit function here that's a wrapper, but I'm not sure if it's necessary. You could just expose the socket itself in the context. I just choose not to.
return (
<SocketContext.Provider value={{ emit, setListener, removeListener, addReconnectEmit, removeReconnectEmit }}>
{children}
</SocketContext.Provider>
)
}
And then in my components, in addition to having the emits to join rooms or conduct actions, I also provide the add and remove ReconnectEmit functions:
const addReconnectEmit = (event, data) => {
reconnectEmits.current = ([...reconnectEmits.current, { event, data }]);
console.log(reconnectEmits.current);
}
const removeReconnectEmit = (event, data) => {
console.log('removing reconnect event');
reconnectEmits.current = reconnectEmits.current.filter(e =>
{ return e.event !== event && e.data !== data }
);
console.log(reconnectEmits.current);
};
With these, I can set it so that, after a reconnect, my socket knows to reconnect to a certain room, etc. See here:
const Chatroom = ({ convoId }) => {
console.log("RENDERED: Chatroom");
const { emit, addReconnectEmit, removeReconnectEmit } = useContext(SocketContext);
useEffect(() => {
emit('joinConvo', convoId);
console.log("Emitting joinConvo message.");
addReconnectEmit('joinConvo', convoId);
return () => {
emit('leaveConvo', convoId);
removeReconnectEmit('leaveConvo', convoId);
}
}, [convoId, emit, addReconnectEmit, removeReconnectEmit]);
return (
<div id="chatroom">
<ChatroomOutput />
<ChatroomStatus />
<ChatroomControls convoId={convoId} />
</div>
);
}
I hope that helps! Between useEffect and manual reconnection logic, I just fixed similar issues to the ones you were having, where I was losing data on reconnection.
Saw you just answered yourself but my approach might still be valuable for others or if you continue to build a socket-client.
You need to abstract the listening components away from the socket object. The socket object upon onMessage needs to retrieve the subscribers and publish the new message to them. You can of course add filtering based on id, type or other properties. Also each component can drop its subscription when un-mounting or based on another need.
In order to show case I used timers but would be easily converted to messages.
socket_io.js
let socket;
const subscribers = []
if (!socket) {
// socket initial connect
socket = true
setInterval(() => {
console.log('interval runs', { socket })
if (socket) {
subscribers.forEach((sub) => {
sub.onMessage()
})
}
}, 1000)
setTimeout(() => {
// socket disconnects
socket = false
setTimeout(() => {
// socket reconnects
socket = true
}, 4000)
}, 4000)
}
export default subscribers;
MessagePage.js
import React, { useEffect, useState } from 'react'
import subscribers from './socket_io.js'
const MessagePage = () => {
const [messageCount, setMessageCount] = useState(0)
let messageHandler = (data) => {
setMessageCount((current) => current + 1)
}
useEffect(() => {
subscribers.push({
id: '1',
onMessage: (data) => messageHandler(data)
})
return () => {
const subToRemove = subscribers.findIndex((sub) => sub.id === '1')
subscribers.splice(subToRemove, 1)
}
}, []);
return (
<div>
Messages received: {messageCount}
</div>
)
}
export default MessagePage
Hope I could help.
export default expects a Hoistable Declarative , i.e function,express
socket_oi.js
import EndPoints from './http/endpoints';
import io from "socket.io-client";
const socketUrl = EndPoints.SOCKET_BASE;
let socketOptions = { transports: ["websocket"] }
let socket;
class Socket {
constructor (){
if (!socket) {
socket = io(socketUrl, socketOptions);
socket.on('connect', () => {
console.log(`Connected to Server`);
})
socket.on('disconnect', () => {
console.log(`Disconnected from Server`);
})
}
socket = this
}
}
//Freeze the object , to avoid modification by other functions/modules
let newSocketInstance = Object.freeze(new Socket)
module.exports = newSocketInstance;
MessagePage.js
import socket from '../socket_io.js';
const MessagePage = (props){
const messageHandler=(data)=>{
}
useEffect(()=>{
socket.on('message',messageHandler); //This event no longer fires When the
singleton socket io instance is reconnected
return ()=>{
socket.off('message');
}
},[]);
}
I'm building a web-based remote control application for the music program Ableton Live. The idea is to be able to use a tablet on the same local network as a custom controller.
Ableton Live runs Python scripts, and I use this library that exposes the Ableton Python API to Node. In Node, I'm building an HTTP/Websocket server to serve my React frontend and to handle communication between the Ableton Python API and the frontend running Redux/RTK Query.
Since I both want to send commands from the frontend to Ableton Live, and be able to change something in Ableton Live on my laptop and have the frontend reflect it, I need to keep a bi-directional Websocket communication going. The frontend recreates parts of the Ableton Live UI, so different components will care about/subscribe to different small parts of the whole Ableton Live "state", and will need to be able to update just those parts.
I tried to follow the official RTK Query documentation, but there are a few things I really don't know how to solve the best.
RTK Query code:
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react';
import { LiveProject } from '../models/liveModels';
export const remoteScriptsApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:9001' }),
endpoints: (builder) => ({
getLiveState: builder.query<LiveProject, void>({
query: () => '/completeLiveState',
async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) {
const ws = new WebSocket('ws://localhost:9001/ws');
try {
await cacheDataLoaded;
const listener = (event: MessageEvent) => {
const message = JSON.parse(event.data)
switch (message.type) {
case 'trackName':
updateCachedData(draft => {
const track = draft.tracks.find(t => t.trackIndex === message.id);
if (track) {
track.trackName = message.value;
// Components then use selectFromResult to only
// rerender on exactly their data being updated
}
})
break;
default:
break;
}
}
ws.addEventListener('message', listener);
} catch (error) { }
await cacheEntryRemoved;
ws.close();
}
}),
})
})
Server code:
import { Ableton } from 'ableton-js';
import { Track } from 'ableton-js/ns/track';
import path from 'path';
import { serveDir } from 'uwebsocket-serve';
import { App, WebSocket } from 'uWebSockets.js';
const ableton = new Ableton();
const decoder = new TextDecoder();
const initialTracks: Track[] = [];
async function buildTrackList(trackArray: Track[]) {
const tracks = await Promise.all(trackArray.map(async (track) => {
initialTracks.push(track);
// A lot more async Ableton data fetching will be going on here
return {
trackIndex: track.raw.id,
trackName: track.raw.name,
}
}));
return tracks;
}
const app = App()
.get('/completeLiveState', async (res, req) => {
res.onAborted(() => console.log('TODO: Handle onAborted error.'));
const trackArray = await ableton.song.get('tracks');
const tracks = await buildTrackList(trackArray);
const liveProject = {
tracks // Will send a lot more than tracks eventually
}
res.writeHeader('Content-Type', 'application/json').end(JSON.stringify(liveProject));
})
.ws('/ws', {
open: (ws) => {
initialTracks.forEach(track => {
track.addListener('name', (result) => {
ws.send(JSON.stringify({
type: 'trackName',
id: track.raw.id,
value: result
}));
})
});
},
message: async (ws, msg) => {
const payload = JSON.parse(decoder.decode(msg));
if (payload.type === 'trackName') {
// Update track name in Ableton Live and respond
}
}
})
.get('/*', serveDir(path.resolve(__dirname, '../myCoolProject/build')))
.listen(9001, (listenSocket) => {
if (listenSocket) {
console.log('Listening to port 9001');
}
});
I have a timing issue where the server's ".ws open" method runs before the buildTrackList function is done fetching all the tracks from Ableton Live. These "listeners" I'm adding in the ws-open-method are callbacks that you can attach to stuff in Ableton Live, and the one in this example will fire the callback whenever the name of a track changes. The first question is if it's best to try to solve this timing issue on the server side or the RTK Query side?
All examples I've seen on working with Websockets in RTK Query is about "streaming updates". But since the beginning I've thought about my scenario as needing bi-directional communication using the same Websocket connection the whole application through. Is this possible with RTK Query, and if so how do I implement it? Or should I use regular query endpoints for all commands from the frontend to the server?
I'm having an issue where my React app is outputting the same one ID on every page that it loads, where as Backend Node.js Socket.io server is outputting multiple client IDs whenever I change route in my app...
Server:
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:3000",
},
});
io.on("connection", (socket) => {
console.log("client connected: ", socket.id);
socket.on("disconnect", (reason) => {
console.log("disconnect", reason);
});
});
App.js:
useEffect(() => {
console.log("use effect", socket.id);
return () => {
socket.off("connect");
socket.off("disconnect");
};
}, []);
Socket.ts:
import io from "socket.io-client";
const socket = io(`${process.env.NEXT_PUBLIC_SOCKET_IO_URL}`);
socket.on("connect", () => console.log("socket_id", socket.id));
export default socket;
server.js (backend - websocket)
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:3000",
},
});
io.on("connection", (socket) => {
console.log("client connected: ", socket.id);
socket.on("disconnect", (reason) => {
console.log("disconnect", reason);
});});
First of all, socket.io server sometimes generates a new id due reconnections or others things:
https://socket.io/docs/v4/server-socket-instance/#socketid
So if you are planning keep the same id, i disencourage you.
Well, but looks you are wondering about fast recriation of ids. React is rendering according state changes, so create a stateless connection inside some component cause this behaviour. Yon can choose a lot of solutions but, i will present you a solution extensible, ease to mantain and deliver to componets the role of subscribe and unsubscribe to itself listeners, this sounds better than have a global listeners declaration :D
1. Create Socket Context
We will use useContext hook to provide SocketContext to entire app.
Create a file in context/socket.js:
import React from "react"
import socketio from "socket.io-client"
import { SOCKET_URL } from "config"
export const socket = socketio.connect(SOCKET_URL)
export const SocketContext = React.createContext()
2. Use socket context and provide a value
Add SocketContext provider at the root of your project or at the largest scope where socket is used:
import {SocketContext, socket} from 'context/socket';
import Child from 'components/Child';
const App = () => {
return (
<SocketContext.Provider value={socket}>
<Child />
<Child />
...
</SocketContext.Provider
);
};
3. Now you can use socket in any child component
For example, in GrandChild component, you can use socket like this:
import React, {useState, useContext, useCallback, useEffect} from 'react';
import {SocketContext} from 'context/socket';
const GrandChild = ({userId}) => {
const socket = useContext(SocketContext);
const [joined, setJoined] = useState(false);
const handleInviteAccepted = useCallback(() => {
setJoined(true);
}, []);
const handleJoinChat = useCallback(() => {
socket.emit("SEND_JOIN_REQUEST");
}, []);
useEffect(() => {
// as soon as the component is mounted, do the following tasks:
// emit USER_ONLINE event
socket.emit("USER_ONLINE", userId);
// subscribe to socket events
socket.on("JOIN_REQUEST_ACCEPTED", handleInviteAccepted);
return () => {
// before the component is destroyed
// unbind all event handlers used in this component
socket.off("JOIN_REQUEST_ACCEPTED", handleInviteAccepted);
};
}, [socket, userId, handleInviteAccepted]);
return (
<div>
{ joined ? (
<p>Click the button to send a request to join chat!</p>
) : (
<p>Congratulations! You are accepted to join chat!</p>
) }
<button onClick={handleJoinChat}>
Join Chat
</button>
</div>
);
};
What is useContext?
useContext provides a React way to use global state,
You can use context in any child component,
Context values are states. React notices their change and triggers re-render.
What is useCallback? Why did you put every handlers inside useCallback?
useCallback prevents reassigning whenever there is state update
Functions will be reassigned only when elements in the second argument are updated
More reference:
https://www.w3schools.com/react/react_usecallback.asp
This nice tutorial has obtained in:
https://dev.to/bravemaster619/how-to-use-socket-io-client-correctly-in-react-app-o65
I am creating a chat app in react, expressjs and socket.io. When I click on Send Button, I am emitting an event and listening that event on server side and again emitting another event from server side and listening that event on client side. And I have written the event listening code on componentDidMount. But don't know why my client side calling same event multiple times. Below is my both side code:
Client side
var socketIOClient = require('socket.io-client')('http://localhost:4001');
sendMessageClicked(e) {
e.preventDefault();
let message = this.state.originalMessage;
var data = {
message: message,
time: Date.now()
}
socketIOClient.emit('send_message',data);
}
componentDidMount() {
socketIOClient.on('msg',function(result){
let messageHtml = 'messages working!';
let messageBox = document.getElementById('messageBox');
if (messageBox ) {
messageBox.appendChild(messageHtml);
}
})
}
render(){
return(
<div>
<form onSubmit={this.sendMessageClicked}>
<textarea onChange={this.handleMessageChange} name="originalMessage" value={this.state.originalMessage}></textarea>
<input type="submit" value="" />
</form>
</div>
);
}
Server side
const app = require('./app');
var server = require('http').Server(app);
var io = require('socket.io')(server);
server.listen(4001);
io.on('connection',function(socket){
socket.on('send_message',function(data){
io.emit('msg',data);
})
})
Can anyone please help with the same?
I had the same issue, and I solved it with useEffect hook.
in your case it would be (on client side):
useEffect(()=>{
socket.on('msg', (result) => {
let messageHtml = 'messages working!';
let messageBox = document.getElementById('messageBox');
if (messageBox ) {
messageBox.appendChild(messageHtml);
})
return function cleanup() {socket.off('msg')}
},[])
I'm sure you could do this also with ComponentDidUpdate or ComponentDidUnmount, but useEffect is eassier.
In the useEffect() hook, checking if socket connection already exists using socket.current helped me get rid of this problem -
useEffect(() => {
(async () => {
if (!socket.current) {
socket.current = io(`http://localhost:8080/`)
.on("connect", () => {
//do something
});
socket.current.on("message", (instance) => {
//receive message from server
});
}
})();
});
I have an API build using sailsjs and a react redux attach to a nodejs backend, and i am trying to implement socket.io for a realtime communication, how does this work?
is it
socket.io client on the react side that connects to a socket.io server on its nodejs backend that connects to a socket.io server on the API
socket.io client on the react side and on its nodejs backend that connects to a socket.io server on the API
i have tried looking around for some answers, but none seems to meet my requirements.
to try things out, i put the hello endpoint on my API, using the sailsjs realtime documentation, but when i do a sails lift i got this error Could not fetch session, since connecting socket has no cookie (is this a cross-origin socket?) i figure that i need to pass an auth code inside the request headers Authorization property.
Assuming i went for my #1 question, and by using redux-socket.io,
In my redux middleware i created a socketMiddleware
import createSocketIoMiddleware from 'redux-socket.io'
import io from 'socket.io-client'
import config from '../../../config'
const socket = io(config.host)
export default function socketMiddleware() {
return createSocketIoMiddleware(
socket,
() => next => (action) => {
const { nextAction, shuttle, ...rest } = action
if (!shuttle) {
return next(action)
}
const { socket_url: shuttleUrl = '' } = config
const apiParams = {
data: shuttle,
shuttleUrl,
}
const nextParams = {
...rest,
promise: api => api.post(apiParams),
nextAction,
}
return next(nextParams)
},
)
}
and in my redux store
import { createStore, applyMiddleware, compose } from 'redux'
import createSocketIoMiddleware from 'redux-socket.io'
...
import rootReducers from '../reducer'
import socketMiddleware from '../middleware/socketMiddleware'
import promiseMiddleware from '../middleware/promiseMiddleware'
...
import config from '../../../config'
export default function configStore(initialState) {
const socket = socketMiddleware()
...
const promise = promiseMiddleware(new ApiCall())
const middleware = [
applyMiddleware(socket),
...
applyMiddleware(promise),
]
if (config.env !== 'production') {
middleware.push(DevTools.instrument())
}
const createStoreWithMiddleware = compose(...middleware)
const store = createStoreWithMiddleware(createStore)(rootReducers, initialState)
...
return store
}
in my promiseMiddleware
export default function promiseMiddleware(api) {
return () => next => (action) => {
const { nextAction, promise, type, ...rest } = action
if (!promise) {
return next(action)
}
const [REQUEST, SUCCESS, FAILURE] = type
next({ ...rest, type: REQUEST })
function success(res) {
next({ ...rest, payload: res, type: SUCCESS })
if (nextAction) {
nextAction(res)
}
}
function error(err) {
next({ ...rest, payload: err, type: FAILURE })
if (nextAction) {
nextAction({}, err)
}
}
return promise(api)
.then(success, error)
.catch((err) => {
console.error('ERROR ON THE MIDDLEWARE: ', REQUEST, err) // eslint-disable-line no-console
next({ ...rest, payload: err, type: FAILURE })
})
}
}
my ApiCall
/* eslint-disable camelcase */
import superagent from 'superagent'
...
const methods = ['get', 'post', 'put', 'patch', 'del']
export default class ApiCall {
constructor() {
methods.forEach(method =>
this[method] = ({ params, data, shuttleUrl, savePath, mediaType, files } = {}) =>
new Promise((resolve, reject) => {
const request = superagent[method](shuttleUrl)
if (params) {
request.query(params)
}
...
if (data) {
request.send(data)
}
request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body))
},
))
}
}
All this relation between the middlewares and the store works well on regular http api call. My question is, am i on the right path? if i am, then what should i write on this reactjs server part to communicate with the api socket? should i also use socket.io-client?
You need to add sails.io.js at your node server. Sails socket behavior it's quite tricky. Since, it's not using on method to listen the event.
Create sails endpoint which handle socket request. The documentation is here. The documentation is such a pain in the ass, but please bear with it.
On your node server. You can use it like
import socketIOClient from 'socket.io-client'
import sailsIOClient from 'sails.io.js'
const ioClient = sailsIOClient(socketIOClient)
ioClient.sails.url = "YOUR SOCKET SERVER URL"
ioClient.socket.get("SAILS ENDPOINT WHICH HANDLE SOCKET", function(data) {
console.log('Socket Data', data);
})