Right way to fetch data with react using socket.io - javascript

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;

Related

How to invalidate useQuery cache from another component?

I have a parent component which needs to invalidate the query cache of a child component:
const Child = () => {
const { data } = useQuery('queryKey', () => fetch('something'))
return <Text>{data}</Text>
}
const Parent = () => {
const queryClient = useQueryClient()
useEffect(() => {
console.log('Clean up happened')
return () => queryClient.invalidateQueries(['queryKey'])
})
return <Child />
}
I can see that Clean up happpened is logged out, but the query cache for queryKey is not invalidated.
Is there something wrong with how I am using #invalidateQueries? Or that query cache of a component (Child) cannot be invalidated by another component (Parent)
From the official documentation, you should use:
const Parent = () => {
const queryClient = useQueryClient()
useEffect(() => {
console.log('Clean up happened')
return () => {
queryClient.invalidateQueries({ queryKey: ['queryKey'] })
};
})
return <Child />
}
That is, of course, if you are using the latest version.

Using use-State hook in react function always throws an error + How to share a variable with another component

I'm a beginner to programming in Javascript/Typescript and just about to develop my very first app in react. To get some data from the backend (FastAPI) I created the function "GetData" that is executed whenever the button is clicked.
Component 1: Button
import { GetData } from "./socket"
export default function Button() {
return (
<button onClick={GetData}>
Run
</button>
)
}
Component 2 (named socket): websocket and data logic
import {useState, createContext} from 'react';
let socket: any;
export async function ConnectWebsocket() {
socket = new WebSocket("ws://127.0.0.1:8000/");
socket.onopen = () => {
console.log('connected')
}
socket.onclose = () => {
console.log('closed')
}
socket.onerror = () => {
console.log('error')
}
}
export async function GetData() {
const [data, setData] = useState({});
socket.send("get Data");
socket.onmessage = (event: any) => {
const newData = JSON.parse(event.data);
console.log(`Data from server: ${newData}`);
setData((data) => ({ ...data, ...newData }));
}
console.log(`Overall data: ${data}`);
}
The problem I'm facing is the useState hook. Whenever I try to stream the data via the websocket by clicking the Run-Button I always get the following error:
Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
I also created a small example using useState-hook and this one worked. Can you spot what I messed up in my code above?
Plus I have another beginner question. How would you make the "data" variable available to a third component (e.g. a table)?
You can only use react hooks inside the actually react component. In your case your Button component. So I would do something like this instead:
class SocketHelper {
socket = new WebSocket("ws://127.0.0.1:8000/");
constructor() {
socket.onopen = () => {
console.log('connected')
}
socket.onclose = () => {
console.log('closed')
}
socket.onerror = () => {
console.log('error')
}
}
}
export const socketHelper = new SocketHelper();
export default function Button() {
const [data, setData] = useState({});
useEffect(() => {
socketHelper.socket.onmessage = (event: any) => {
const newData = JSON.parse(event.data);
console.log(`Data from server: ${newData}`);
setData((data) => ({ ...data, ...newData }));
}
}, []);
const getData = () => {
socketHelper.socket.emit("getdata");
}
return (
<div>
<button onClick={getData}>
Run
</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
Also you are using socket.send but it doesn't seems like your are using the returned socket. So instead i would use the emit function: https://socket.io/docs/v4/client-api/#socketemiteventname-args

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

useEffect runs twice as much for each message sent

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.

SocketIO & ReactJs: Socket Emitting multiple times

My client is responding multiple times when my server emits an event.
By reading other available SOF answers, it seems like I must have duplicated the event listeners somewhere in my code.
I actually have other socket listeners in the same file, and they all (?) seem to be functioning fine.
I've tried moving the listener out of the useEffect to see if it works, but it has the same issue.
I've tried to limit the listener to socket.once, however, there is an additional issue -
user is not defined on the first emit.
Only the second emit then user is defined...
Client:
const getUserInfo= async () => {
try {
const { data } = await axios.get('http://localhost:8080/user/info', axiosConfig);
return data;
} catch (error) {
return { error };
}
}
const App = ({ socket, clientId }) => {
const [state, setState] = useState({
user: undefined,
});
const checkAuthentication = async () => {
const user= await getUserInfo();
localStorage.setItem('user.email', user.email);
setState({ ...state, user});
return true;
}
useEffect(() => {
const onMount = async () => {
const authed = await checkAuthentication()
};
onMount();
}, []);
userEffect(() => {
socket.on("allInfo", async ({ requester }) => { //This catches multiple times
console.log(`Admin ${requester} requested for tablet information.`)
const user= { ...state.user};
socket.emit('infoPayload', { user); //thus this emits multiple times.
})
}, [state.user])
return (
<>
<Route exact path="/"
render={() => <div>{state.user&& state.user.email}</div>}
/>
</>
);
}
export default (App);

Categories

Resources