I am building a simple chat app with react, express and socket.io
I got stuck on receiving message from backend server.
Every time user receive some message, the useEffect will runs approximately twice as much as before so after 5 or 6 received messages the app start really slow down.
useEffect(() => {
socket.on('mes', (data) => {
setChat([...chat, data]);
});
}, [chat]);
any idea how to make it run just once every time user receive a message?
Whole code
import Chat from '../Chat/chat';
import queryString from 'query-string';
let socket;
const ChatRoom = ({ location }) => {
const [name, setName] = useState('');
const [room, setRoom] = useState('');
const [message, setMessage] = useState('');
const [chat, setChat] = useState([]);
const ENDPOINT = 'http://localhost:4001/';
useEffect(() => {
const { name, room } = queryString.parse(location.search);
setName(name);
setRoom(room);
socket = socketIOClient(ENDPOINT);
socket.emit('join', { name, room });
return () => {
socket.emit('disconnect');
socket.disconnect();
};
}, [ENDPOINT, location.search]);
const click = (e) => {
e.preventDefault();
socket.emit('message', message);
setMessage('');
};
useEffect(() => {
socket.on('mes', (data) => {
setChat([...chat, data]);
});
}, [chat]);
return (
<div className="ChatRoom-Container">
{chat.map((mes, index) => {
return <Chat text={mes.text} user={mes.user} key={index}></Chat>;
})}
<input
value={message}
className="ChatRoom-Input"
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
return e.key === 'Enter' ? click(e) : null;
}}
></input>
</div>
);
};
export default ChatRoom;
Use setChat(prev => next) instead of setChat(value) so you don't have to reference the previous value from the closure:
useEffect(() => {
socket.on('mes', (data) => {
setChat(prev => [...prev, data]);
});
}, []);
useEffect(() => {
socket.on('mes', (data) => {
setChat([...chat, data]);
});
}, [chat]);
Here in your dependency array you have the chat variable as a dependency.
According to rules when the value of chat changes the effect runs.
as a result after you call setChat() one time it changes the value of chat and as a result it runs the effect one more time. that's why your effect is being called twice.
Related
I'm creating lobby for game and I want to display player list. I'm watching for new players in room.
const [roomUsers, setRoomUsers] = useState([])
useEffect(() => {
if (room)
DB.collection("room_users").where("room_id", "==", room.id)
.onSnapshot((querySnapshot) => {
const newRoomUsers = [];
querySnapshot.forEach((doc) => {
newRoomUsers.push(doc.data());
});
setRoomUsers(newRoomUsers)
});
})
When I join in the second window, the number of players changes but the list updates over time. When I join more times, the list stops updating at all.
<h4>Players list ({roomUsers.length}/4)</h4>
{roomUsers.map(roomUser => (
<PlayerListItem
roomUser={roomUser}
owner={roomUser.uuid === room.owner}
/>
))}
In PlayerListItem component I am fetching the user via uuid
const [user, setUser] = useState(null)
useEffect(() => {
DB.collection('users').where('uuid', '==', roomUser.uuid).get().then(snapshot => {
if (!snapshot.empty)
setUser(snapshot.docs[0].data())
})
}, [])
Then I return the username through the component.
Sometimes the same username appears twice in the list instead of different
Your useEffect is missing the dependencies array. It should also return the cleanup function
useEffect(() => {
if (!room) {
return;
}
const unsub = DB.collection("room_users").where("room_id", "==", room.id)
.onSnapshot((querySnapshot) => {
const newRoomUsers = [];
querySnapshot.forEach((doc) => {
newRoomUsers.push(doc.data());
});
setRoomUsers((currVal) => {
console.log({currVal, newRoomUsers})
const newVal = [] // change this to be correct
return newVal
})
});
return () => unsub();
}, [room])
I have a React application which uses a Django backend, I have used webSocket to connect with the backend which updates state when there are some changes. But the changes are very rapid, so only the last changes are visible. I want to show the previous message for a certain time before next message is displayed. Here is my code
import React, { useEffect, useState, useRef } from "react";
const Text = () => {
const [message, setMessage] = useState("");
const webSocket = useRef(null);
useEffect(() => {
webSocket.current = new WebSocket("ws://localhost:8000/ws/some_url/");
webSocket.current.onmessage = (res) => {
const data = JSON.parse(res.data);
setMessage(data.message);
};
return () => webSocket.current.close();
}, []);
return <p>{message}</p>;
};
export default Text;
So the message should be visible for certain time (in seconds, for eg - 5 seconds), then the next message should be shown. Any idea how that could be done?
const Text = () => {
const [messages, setMessages] = useState([]);
const currentMessage = messages[0] || "";
const [timer, setTimer] = useState(null);
// webSocket ref missing? ;-)
useEffect(() => {
webSocket.current = new WebSocket("ws://localhost:8000/ws/some_url/");
webSocket.current.onmessage = (res) => {
const data = JSON.parse(res.data);
setMessages((prevState) => [ ...prevState, data.message]);
};
return () => webSocket.current.close();
}, []);
// Remove the current message in 5 seconds.
useEffect(() => {
if (timer || !messages.length) return;
setTimer(setTimeout(() => {
setMessages((prevState) => prevState.slice(1));
setTimer(null);
}, 5000));
}, [messages, timer]);
return <p>{currentMessage}</p>;
};
You can create a custom hook to handle the message transition. Pass as argument the desired time you want to wait before showing the next message. You can use it in other parts of your code:
useQueu.js
const useQueu = time => {
const [current, setCurrent] = useState(null); //--> current message
const queu = useRef([]); //--> messages
useEffect(() => {
const timeout = setTimeout(() => {
setCurrent(queu.current.shift());
}, time);
return () => clearTimeout(timeout);
}, [current]);
const add = obj => {
if (!current) setCurrent(obj); //--> don't wait - render immediately
else {
queu.current.push(obj);
}
};
return [current, add];
};
Text.js
const Text = () => {
const [message, add] = useQue(5000);
const webSocket = useRef(null);
useEffect(() => {
webSocket.current = new WebSocket("ws://localhost:8000/ws/some_url/");
webSocket.current.onmessage = (res) => {
const data = JSON.parse(res.data);
add(data.message); //--> add new message
};
return () => webSocket.current.close();
}, []);
return <p>{message}</p>;
};
Working example
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
I got stuck with the following and haven't found any answer after a lot of research.
What I want to do: simply getting users inluding their images from a firestore-DB with react and the useeffect-hook and displaying them.
The DB-structure looks as follows:
https://i.stack.imgur.com/sDcrv.png
So the pictures are a subcollection of the users-collection.
After getting the users from the users-collection, I'm doing a second request for adding the users images to this specific user using Object.assign. After every forEach-run over the users-collection I'm setting the users-array with setUsers((oldUsers) => [...oldUsers, currentUser]);. Logging the users-array shows uses INCLUDING their images.
The problem: When trying to render the images, they are always undefined.
Workaround: Pressing a button that calls a function for re-setting the users:
const reRenderUsers = () => {
if (userDataLoaded === false) {
setUserDataLoaded(true);
}
const copy = [...users];
setUsers(copy);
};
^ This solves the problem and all images where shown.
Question: Is there any possibility showing the images instantly without the need of "re-rendering" the users? Am I using the useEffect-hook wrong for example? I'm thankful for any advice. Many thanks in advance!
Here the full code:
const [users, setUsers] = useState([]);
const [userDataLoaded, setUserDataLoaded] = useState(false);
useEffect(() => {
const unsubscribe = database.collection("users").onSnapshot((snapshot) => {
snapshot.forEach((doc) => {
const currentUser = {
id: doc.id,
...doc.data(),
};
database
.collection("users")
.doc(currentUser.id)
.collection("pictures")
.get()
.then((response) => {
const fetchedPictures = [];
response.forEach((document) => {
const fetchedPicture = {
id: document.id,
...document.data(),
};
fetchedPictures.push(fetchedPicture);
});
currentUser.pictures = [];
Object.assign(currentUser.pictures, fetchedPictures);
})
.catch((error) => {
console.log(error);
});
setUsers((oldUsers) => [...oldUsers, currentUser]);
});
});
return () => {
unsubscribe();
};
}, []);
const reRenderUsers = () => {
if (userDataLoaded === false) {
setUserDataLoaded(true);
}
const copy = [...users];
setUsers(copy);
};
return (
<div>
{!userDataLoaded ? (
<button onClick={reRenderUsers}> load users </button>
) : null}
{users.map((user, index) => (
<div key={user.id}>
{user.pictures && <img src={user.pictures[0].imageUrl}></img>}
</div>
))}
</div>
);
}
export default User;
This is because you are calling setUser before the firebase response completes the callback chain. You need to update the state right after the loop inside the success callback completed. I have updated useEffect to update it right after the callback
useEffect(() => {
const unsubscribe = database.collection("users").onSnapshot((snapshot) => {
snapshot.forEach((doc) => {
const currentUser = {
id: doc.id,
...doc.data(),
};
database
.collection("users")
.doc(currentUser.id)
.collection("pictures")
.get()
.then((response) => {
const fetchedPictures = [];
response.forEach((document) => {
const fetchedPicture = {
id: document.id,
...document.data(),
};
fetchedPictures.push(fetchedPicture);
});
currentUser.pictures = fetchedPictures;
setUsers((oldUsers) => [...oldUsers, currentUser]);
})
.catch((error) => {
console.log(error);
});
//dont need this here
//setUsers((oldUsers) => [...oldUsers, currentUser]);
});
});
return () => {
unsubscribe();
};
}, []);
Good Luck
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;