How to reconnect socket instance from Socket.io after page refresh on React page? - javascript

Trying to make a simple chat app with express + socket.io and react but this issue has been racking my brain. Whenever I refresh the page on messaging app route, I can't send any messages. I tried circumventing that by using react-router with the history API to send me back to homepage. However, when I'm routed back to the homepage, I can't re-enter into the chat page unless I refresh again on the homepage. It seems that the socket object is null whenever I refresh the page.
Server Code:
const room = "general";
const room2 = "memes";
const LOGIN = "loginEvent";
const NEW_MESSAGE_EVENT = "newMessageEvent";
const LEAVE_EVENT = "leaveEvent";
io.on("connection", (socket) => {
socket.on(LOGIN, ({ nickname, room }, callback) => {
const user = addUser(socket.id, nickname, room);
socket.join(user.room);
callback();
// socket.emit()
});
socket.on(NEW_MESSAGE_EVENT, (data) => {
console.log(data);
io.in(room).emit(NEW_MESSAGE_EVENT, data);
});
socket.on("disconnect", () => {
const deletedUser = delUser(socket.id);
console.log("User deleted:", deletedUser);
if (deletedUser) {
io.in(room).emit(LEAVE_EVENT, {
notification: `User ${deletedUser.nickname} has just left`,
});
}
socket.leave(room);
});
});
In my client-side, I use the react context API to store the socket object like so:
import React, { createContext } from "react";
import io from "socket.io-client";
const SocketContext = createContext();
const SocketProvider = ({ children }) => {
const ENDPOINT = "http://localhost:3030";
const socket = io(ENDPOINT);
return (
<SocketContext.Provider value={socket}>
{children}
</SocketContext.Provider>
);
};
export { SocketContext, SocketProvider };
Then, I create a hook to send messages:
import { useState, useContext, useEffect } from "react";
import { SocketContext } from "../contexts/socketContext";
const NEW_MESSAGE_EVENT = "newMessageEvent";
const useRoom = () => {
const socket = useContext(SocketContext);
const [messages, setMessages] = useState([]);
useEffect(() => {
socket.on(NEW_MESSAGE_EVENT, (message) => {
const incomingMessage = {
...message,
isOwner: message.id === socket.id,
};
setMessages((messages) => [...messages, incomingMessage]);
});
return () => {
socket.close();
};
}, [socket]);
const sendMessage = (message) => {
socket.emit(NEW_MESSAGE_EVENT, { body: message, id: socket.id });
};
return { messages, sendMessage };
};
export default useRoom;

Related

Getting an issue to implement load more feature using node mongo express react

I am trying to implement load more feature in my react application. And for that I am creating an endpoint to implement load more. So, whenever app load it is fetching initial 5 data but unable to fetch next 5 data after clicking on load more button from UI.
Here is controller for that
export const loadMoreBlogs = async (req, res) => {
const { skip } = req.body;
try {
const limit = 5;
const blogs = await BlogModel.find().skip(skip).limit(limit);
res.status(200).json(blogs);
} catch (error) {
res.status(404).json({ message: error.message });
}
};
React component
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { loadMoreBlogs, setLoadedBlogs } from "../redux/features/blogSlice";
const Blogs = () => {
const [skip, setSkip] = useState(0);
const dispatch = useDispatch();
const limit = 5;
const { loadBlogs } = useSelector((state) => ({ ...state.blog }));
useEffect(() => {
dispatch(loadMoreBlogs(skip));
}, [skip]);
console.log("skip", skip);
const handleLoadMore = () => {
const skipTo = skip + limit;
dispatch(loadMoreBlogs(skipTo));
setSkip(skipTo);
setLoadedBlogs(loadBlogs);
};
return (
<>
{loadBlogs?.map((item) => (
<h2>{item.title}</h2>
))}
<button onClick={handleLoadMore}>Load More</button>
</>
);
};
export default Blogs;

React Native: How to access a variable from parent component in child component?

i am trying to pass my newsocket variable from my MessagesScreen.js to my ChatScreen.js.
I have been stuck on this point for a while and would appreciate any help possible. What i am trying to achieve is that only one connection gets emitted which i can listen to events on both screen.
The connection is now opened on the messagesScreen. My problem now is if user 1 is on the allmessages screen and user 2 is inside the chat. And user 2 sends user 1 a message, user 1's screen does not automatically update with the last message for the conversation the message was sent to, I need to either scroll to refresh or navigate from one page to the other in order for it to appear.
Here is my code:
PARENT --> messagesScreen.js
function MessagesScreen({navigation}) {
const [posts, setPosts] = useState([]);
const { user } = useAuth();
const [socket, setSocket] = useState(null);
const loadposts = async () => {
const response = await messagesApi.getMessages();// here i am loading all the conversation this user has
setPosts(response.data)
};
useEffect(() => {
newsocket = sockets(user.id); // newsocket is what i am trying to pass to child component
setSocket(newsocket);
loadPosts()
newsocket.on("send_message", (msg) => {
console.log("messages:", msg);
})
}, []);
return (
<FlatList
data={posts}
keyExtractor={(post) => post.id.toString()}
renderItem={({ item,index }) => (
<MessagesList
title={item.Post.title}
subTitle={item.Messages[0].message}
onPress={() => navigation.navigate(
routes.CHAT,{message:item,index,newsocket:socket})}
/>
)}
/>
)
CHILD ---> chatScreen.js
function ChatScreen({route,navigation,socket}) {
const [messages, setMessages] = useState([]);
const { user } = useAuth();
const index = route.params.index;
const message = route.params.message;
const newsocket = route.params.newsocket;
const loadListings = async () => {
const response = await messagesApi.getConversation(message.id);// here i am loading the messages in that specific conversation
setMessages(response.data.Messages)
};
useEffect(() => {
loadListings()
newsocket.emit('subscribe', message.id);
newsocket.on("send_message", (msg) => {
console.log("this is the chat messages:", msg);
setMessages(messages => [msg, ...messages]);
});
}, []);
const onSend = (ConversationId,senderId,receiverId,message) => {
const to = (user.id===route.params.message.user1?
route.params.message.user2:route.params.message.user1)
socket.emit('message', { to: to, from: user.id, message,ConversationId});
messagesApi.sendMessage({ConversationId,senderId,receiverId,message});
};
return(
<FlatList
inverted
data={messages}
keyExtractor={(item,index)=>index.toString()}
extraData={messages} // add this
renderItem={({item,index})=>(
<MessageBubble
text={item.message}
mine={item.senderId !== user.id}
/>
)}
/>
)
socket.js
import io from 'socket.io-client';
const newsocket = (user) => {
let newsocket = io.connect("http://192.168.1.107:9000")
newsocket.on('connect', msg => {
console.log(`waiting for user: ${user} to join a conversation`)
});
newsocket.emit('waiting', user);
return newsocket;
}
export default newsocket;
I would approach this differently.
You can create your socket connection as a shared service in a separate module and simply import that into the relevant components you need. In this shared module you handle connecting/disconnecting and return an existing connection or create a new connection to return.
Quick rough:
// socket-server.ts
import io from 'socket.io-client';
let socket: SocketIOClient.Socket = null;
export const getSocketServer = (): Promise<SocketIOClient.Socket> => {
return new Promise<SocketIOClient.Socket>(resolve => {
if (socket) {
console.info(`returning existing socket: ${socket.id}`);
return resolve(socket);
}
socket = io('http://localhost:4000', {
autoConnect: false,
});
socket.on('connect_error', (err) => {
console.error(err);
})
socket.on('connect', () => {
console.info(`creating new socket: ${socket.id}`);
return resolve(socket);
});
socket.open();
})
}
// then in your relevant modules
// module-a.ts
import React, {useEffect, useState} from 'react';
import {getSocketServer} from './../components/socket-server';
const Component = () => {
useEffect(() => {
const connect = async () => {
const socket = await getSocketServer();
socket.on('hello', (message) => {
console.info('hello from module A', message);
});
}
connect();
}, []);
return (
<>
<h2>Module A</h2>
</>
)
}
export default Component;
You could maybe also look at creating a Context Provider and share the socket with relevant modules as needed.
Context provides a way to pass data through the component tree without
having to pass props down manually at every level.
On the MessagesScreen screen you are passing the SOCKET function and not the variable it self . i think you do not need the function . you directly pass the variable and access in chatScreen screen .
MessagesScreen.js
routes.CHAT,{message:item,index,updateView, newsocket})}
chatScreen.js
const newsocket = route.params.newsocket;
....
newsocket.emit('subscribe', message.id); // call like this

How do deal with React hooks returning stale event handlers

I'm trying to figure out how to deal with stale event handlers returned by a hook. First when the component is first rendered, it makes a an asynchronous request to api to fetch credentials. Then these credentials are used when pressing a submit button in a dialog to create a resource. The problem is the credentials for the dialog submit button click event handler are undefined even after the credentials have been fetched.
credentials.js
import { useEffect } from 'react';
import { api } from './api';
export const useCredentials = (setCredentials) => {
useEffect(() => {
const asyncGetCredentials = async () => {
const result = await api.getCredentials();
if (result) {
setCredentials(result);
}
};
asyncGetCredentials().then();
}, []);
return credentials;
}
useComponent.js
import { useEffect, useRef, useCallback, useState } from 'react';
import { useCredentials } from './credentials';
import { createResource } from './resources';
import { useDialog } from './useDialog';
export const useComponent = () => {
const { closeDialog } = useDialog();
const [credentials, setCredentials] = useState();
useCredentials(setCredentials);
const credentialsRef = useRef(credentials);
useEffect(() => {
// logs credentials properly after they have been fetched
console.log(credentials)
credentialsRef.current = credentials;
}, [credentials]);
const createResourceUsingCredentials = useCallback(
async function () {
// credentials and credentialsRef.current are both undefined
// even when the function is called after the credentials
// have already been fetched.
console.log(credentials);
console.log(credentialsRef.current);
createResource(credentialsRef.current);
}, [credentials, credentialsRef, credentialsRef.current]
);
const onDialogSubmit = useCallback(
async function () {
await createResourceUsingCredentials();
closeDialog();
}, [
credentials,
credentialsRef,
credentialsRef.current,
createResourceUsingCredentials,
],
);
return {
onDialogSubmit,
}
}
Try this way
export const useCredentials = (setCredentials) => {
useEffect(() => {
const asyncGetCredentials = async () => {
const result = await api.getCredentials();
if (result) {
setCredentials(result);
}
};
asyncGetCredentials().then();
}, []);
}
export const useComponent = () => {
const { closeDialog } = useDialog();
const [credentials, setCredentials] = useState(); // new add
useCredentials(setCredentials);
....
}
Why are you adding complexity, always return function and check inside the function for credentials
export const useComponent = () => {
const { closeDialog } = useDialog();
const credentials = useCredentials();
// correctly logs undefined at first and updated credentials
// when they are asynchronously received from the api.
console.log(credentials);
async function createResourceUsingCredentials() {
createResource(credentials);
}
let onClickDialogSubmit = async () => {
if (credentials) {
await createResourceUsingCredentials();
closeDialog();
}
};
return {
onClickDialogSubmit,
}
}
I found the problem was in the useCredentials hook implementation. It blocks any further requests for credentials to the api if a request is already in flight. Due to poor implementation of this functionality, if more than 1 component using that hook was rendered, only the component that was rendered first got updated credentials. I changed the useCredentials hooks so that it subscribes to the global state (that has the credentials) so that no matter which component starts the request, all components will get the credentials when the request finishes. https://simbathesailor007.medium.com/debug-your-reactjs-hooks-with-ease-159691843c3a helped a lot with debugging this issue.

React hooks array passing in number when passed into props of component

I am currently working on a chat application and for some reason every time I pass in my array of messages as a prop to another component it passes in a number to the component instead of the message object. I have tried a lot of different methods of passing it in regarding using multiple components etc but it seems to still be passing in the number of elements for some reason. Any help is appreciated... code is below
Component receiving the props
import React, { useEffect } from 'react'
import Message from '../../Message/Message'
function Messages({ messages }) {
useEffect(() => {
console.log(messages)
}, [messages])
return (
<div>
test
</div>
)
}
export default Messages
// Import React dependencies.
import React, { useEffect, useState, } from "react";
// Import React dependencies.
import io from 'socket.io-client'
import axios from 'axios'
import Messages from './Messages/Messages'
import uuid from 'react-uuid'
import { Redirect } from 'react-router-dom'
// Import the Slate components and React plugin.
const ENDPOINT = 'http://localhost:5000/'
export const socket = io.connect(ENDPOINT)
const LiveChatFunction = ({ group_id }) => {
// Add the initial value when setting up our state.
const [message, setValue] = useState("")
const [user, setUser] = useState("")
const [groupId, setGroup] = useState('')
const [messages, setMessages] = useState([])
const [toLogin, userAuth] = useState(false)
useEffect(() => {
setGroup(group_id)
axios.post('http://localhost:5000/api/users/refresh_token', null, { withCredentials: true }).then(data => {
if (!data.data.accessToken) {
userAuth(true)
}
})
axios.get('http://localhost:5000/api/users/userInfo', { withCredentials: true }).then(data => {
setUser(data.data.user)
})
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
axios.get(`http://localhost:5000/live/${group_id}`).then(x => {
console.log(x.data)
})
}, [group_id, messages])
function setClick() {
const data = {
messageId: uuid(),
user,
groupId,
message
}
socket.emit('message', data)
}
if (toLogin) {
return (
<Redirect to="/login" />
)
}
return (
<div>
<input placeholder="message" type="text" onChange={value => {
setValue(value.target.value)
socket.emit('typing-message', { username: user, time: new Date() })
}} />
<button onClick={setClick}>Submit</button>
<Messages messages={messages} />
</div>
)
}
export default LiveChatFunction;
I have added some comments of what I think you can change:
useEffect(() => {
const recieveFunction = (data) => {
//using callback so no dependency on messages
setMessages((messages) => messages.push(data));
};
async function init() {
//next line is pointless, this runs when group_id
// has changed so something must have set it
// setGroup(group_id);
await axios //not sure if this should be done before listening to socket
.post(
'http://localhost:5000/api/users/refresh_token',
null,
{ withCredentials: true }
)
.then((data) => {
if (!data.data.accessToken) {
userAuth(true);
}
});
await axios
.get('http://localhost:5000/api/users/userInfo', {
withCredentials: true,
})
.then((data) => {
setUser(data.data.user);
});
//start listening to socket after user info is set
socket.on(`message-${group_id}`, recieveFunction);
axios
.get(`http://localhost:5000/live/${group_id}`)
.then((x) => {
console.log(x.data);
});
}
init();
//returning cleanup function, guessing socket.off exists
return () =>
socket.off(`message-${group_id}`, recieveFunction);
}, [group_id]); //no messages dependencies
console.log('messages are now:',messages);
If messages is still not set correctly then can you log it
So I think I found your problem:
In your useEffect hook, you're setting messages to the wrong thing.
socket.on(`message-${group_id}`, data => {
setMessages(messages.push(data))
});
An example:
const m = [].push();
console.log(m);
// m === 0
const n = [].push({});
console.log(n);
// n === 1
As you can see this is the index.
So what you need is:
socket.on(`message-${group_id}`, data => {
messages.push(data);
setMessages(messages);
});
This will set messages to the array of messages.

Right way to fetch data with react using socket.io

I didn't found any examples about how to fetch data from express server using react with socket.io.
Now i do something like this:
Server.js
io.on('connection', socket => {
console.log(socket.id)
socket.on('disconnect', () => {
console.log(socket.id + ' disconnected')
})
socket.on('load settings', () => {
socket.emit('settings is here', data)
})
})
React.js
const [socket] = useState(io())
const [settings, setSettings] = useState(false)
useEffect(() => {
try {
socket.emit('load settings');
socket.on('settings is here', (data) => {
// we get settings data and can do something with it
setSettings(data)
})
} catch (error) {
console.log(error)
}
}, [])
This looks fine, but there are some things you can improve on, such as disconnecting the socket before unmounting and not making the socket part of state (refer to the code example below).
If you're confused over how to port existing code to hooks, write out the component using classes first, then port part by part to hooks. You could refer to this StackOverflow answer as a cheatsheet.
Using traditional classes, using socket.io looks like:
class App extends React.Component {
constructor(props) {
super(props);
this.socket = io();
}
componentDidMount() {
this.socket.open();
this.socket.emit('load settings');
this.socket.on('settings is here', (data) => {
// we get settings data and can do something with it
this.setState({
settings: data,
})
});
}
componentWillUnmount() {
this.socket.close();
}
render() {
...
}
}
Then you can port the this.socket to use useRef (it doesn't need to be part of state as your render() function doesn't need it. So useRef is a better alternative (although useState is likely to still work).
Port componentDidMount() via using useEffect and passing an empty array as the second argument to make the effect callback only run on mount.
Port componentWillUnmount() via returning a callback function in the useEffect callback which React will call before unmounting.
function App() {
const socketRef = useRef(null);
const [settings, setSettings] = useState(false);
useEffect(() => {
if (socketRef.current == null) {
socketRef.current = io();
}
const {current: socket} = socketRef;
try {
socket.open();
socket.emit('load settings');
socket.on('settings is here', (data) => {
// we get settings data and can do something with it
setSettings(data);
})
} catch (error) {
console.log(error);
}
// Return a callback to be run before unmount-ing.
return () => {
socket.close();
};
}, []); // Pass in an empty array to only run on mount.
return ...;
}
The accepted answer has the downside, that the initial state of the useRef() gets called on every re-render. With a text input for example, a new connection is established on every input change. I came up with two solutions:
Define the socket in the useEffect
const ChatInput = () => {
const [chatMessage, setChatMessage] = useState<string>('');
const socket = useRef<Socket>();
useEffect(() => {
socket.current = io('my api');
socket.current.on('chat message', (message: string) => {
setChatMessage(message);
});
return () => { socket.current?.disconnect(); };
}, []);
const inputHandler = (text: string) => {
socket.current?.emit('chat message', text);
};
return (
<View>
<Text>{chatMessage}</Text>
<TextInput onChangeText={(text) => inputHandler(text)} />
</View>
);
};
Define the socket.io in a useState()
const ChatInput = () => {
const [chatMessage, setChatMessage] = useState<string>('');
const [socket] = useState(() => io('my api'));
useEffect(() => {
socket.on('chat message', (message: string) => {
setChatMessage(message);
});
return () => { socket.disconnect(); };
}, []);
const inputHandler = (text: string) => {
socket.emit('chat message', text);
};
return (
<View>
<Text>{chatMessage}</Text>
<TextInput onChangeText={(text) => inputHandler(text)}/>
</View>
);
};
export default ChatInput;

Categories

Resources