Why Socket.io client in React perform all event two times? - javascript

I am trying to use Socket.io client in a Next.js app. But I am facing one problem; when I perform an event socket runs two times. But I am not finding any solutions. Can anyone help me to debug this problem?
I am using React Context to create a socket provider.
socket.context.tsx-
import { createContext, FC, useContext } from "react";
import { io, Socket } from "socket.io-client";
import { getCookie } from "cookies-next";
const token = getCookie("session")
const socket = io(process.env.NEXT_PUBLIC_WS || "http://localhost:5000", {
extraHeaders: {
"authorization": token?.toString() || ""
}
})
//types
interface SocketContextInterface {
socket: Socket
}
const SocketContext = createContext<SocketContextInterface>({ socket });
interface Props {
children: React.ReactNode
}
const SocketProvider: FC<Props> = (props) => {
return (
<SocketContext.Provider value={{ socket }}>
{props.children}
</SocketContext.Provider>
);
};
export const useSocket = () => useContext(SocketContext)
export default SocketProvider;
And then, I am using it in one component-
const { socket } = useSocket();
//Performing create message on onSubmit handler
const onSubmit: SubmitHandler<Inputs> = (submitData) => {
const MessageData = {
conversation: selected,
message: {
text: submitData.message
}
}
socket.emit("createMessage", MessageData);
reset();
}
//And then I am try to listen event
useEffect(() => {
socket.on("createMessage", (data: MessageData) => {
setMessages((prev: MessageData[]) => [...prev, data])
})
}, [socket])
// But this performs two times
Here socket.on perform two times. How can I solve this problem?

I think your createMessag listener is being registered two times in memory, either because of a re-render or StrictMode. Add a clean-up, like so:
useEffect(() => {
const listener = (data: MessageData) => {
setMessages((prev: MessageData[]) => [...prev, data]);
};
socket.on("createMessage", listener);
return () => {
socket.off("createMessage", listener);
};
}, [socket]);

Related

Not able to listen to a socket event in ReactJs component

If I am trying to call the socket.on outside the component, It is working but when I use it in useEffect() It is not working?
import ChatBubble from "../components/Chatbubblebot";
import io from 'socket.io-client'
let data = [];
function Messagelist({ sendMessage }) {
data.push(sendMessage);
useEffect(() => {
const socket = io.connect("http://localhost:5000")
socket.on("admin",(payload) => {
console.log("payload",payload);
data.push({who:"him",message:payload})
})
})
let list = () => {
console.log("sendMessage",sendMessage.length);
if (data === undefined && sendMessage.length === 0) {
} else {
return data.map((e) => <ChatBubble who={e.who} message={e.message} />);
}
};
return <ul>{list()}</ul>;
}
export default Messagelist;```
Try defining the socket variable outside component.
var socket;
And then in the component useEffect:
socket = io.connect("http://localhost:5000")
Also,
the let data = []; should not be defined like that.
It should be a state variable of the component like below:
const [data, setData] = useState([]);
And when you get new message from socket:
socket.on("admin",(payload) => {
console.log("payload",payload);
let _data= data;
_data.push({who:"him",message:payload});
setData(_data);
})

Notify other component by raising a manual event

A have two files, with two functional components A and B, in the first component A, i have a specialFunction that gets called with onClick, what i want to do is raise an event in specialFunction when it's called, and then in component B add a Listener for the event in specialFunction.
Component A:
function specialFunction(){
//raise the event and send some data
}
Component B:
//contains a listener that does some work when specialFunction is called, example:
(data) => {console.log("am called:",data)};
1. Create notifier class using observer pattern
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
2. ComponentA receives notifier as a prop and used to notify all subscribers
const ComponentA = ({ notifier }) => {
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div>{/** Some content */}</div>
}
3. ComponentB receives notifier and subscribes to it to receive data sent by from ComponentB
const ComponentB = ({ notifier }) => {
useEffect(() => {
const callbackFn = data => {/** Do whatever you want with received data */ }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [])
}
4. App holds both component. Create instance of notifier there and pass as a props
const App = () => {
const dataNotifier = new ChangeNotifier();
return <div>
<ComponentA notifier={dataNotifier} />
<ComponentB notifier={dataNotifier} />
</div>
}
If you have components on different levels deeply nested and it is hard to pass notifier as a prop, please read about React Context which is very helpful when you want to avoid property drilling
React Context
Here's implementation with context
class ChangeNotifier {
subscribers = [];
subscribe(callback) {
this.subscribers.push(callback);
return this.unsubscribe.bind(this, callback);
}
unsubscribe(callback) {
const index = this.subscribers.indexOf(callback);
if (index > -1) {
this.subscribers.splice(index, 1);
}
}
notifyAll(data) {
this.subscribers.forEach(callback => callback(data));
}
}
const NotifierContext = React.createContext();
const ComponentA = () => {
const { notifier } = useContext(NotifierContext);
const triggerNotifier = () => {
notifier.notifyAll('Some data that will subscribers receive');
}
return <div><button onClick={triggerNotifier}>Notify</button></div>
}
const ComponentB = () => {
const { notifier } = useContext(NotifierContext);
useEffect(() => {
const callbackFn = data => { console.log(data) }
notifier.subscribe(callbackFn);
return () => notifier.unsubscribe(callbackFn);
}, [notifier])
}
Now all components wrapped in NotifierContext.Provider (no matter how deep they are nested inside other components) will be able to use useContext hook to receive context value passed as value prop to NotifierContext.Provider
const App = () => {
const dataNotifier = useMemo(() => new ChangeNotifier(), []);
return <NotifierContext.Provider value={{ notifier: dataNotifier }}>
<ComponentA />
<ComponentB />
</NotifierContext.Provider>
}
export default App;
Last but not least, I guess you can avoid context or properties drilling and just create instance of ChangeNotifier in some utility file and export it to use globally...
Andrius posted a really good answer, but my problem was that the two components, one of them is used as an API, and the other had a parent component, am a beginner so maybe there is a way to use them but i just didn't know how.
The solution that i used, (maybe not the best) but did the job was to dispatch a custom event in a Promise from the specialFunction:
function specialFunction(){
new Promise((resolve) => {
console.log("am the promise");
document.dispatchEvent(event);
resolve();
});
And add a Listener in the other component using a useEffect hook:
useEffect(() => {
let handlePreview = null;
new Promise((resolve) => {
document.addEventListener(
"previewImg",
(handlePreview = (event) => {
event.stopImmediatePropagation();
//Stuff...
})
);
return () =>
window.removeEventListener("previewImg", handlePreview, false);
});
}, []);
Thank you for your help.

react hook useContext giving null value in my props.children component

I am new in reactjs hooks. I have a component which opens a socket from the server. Now I want to pass the props from my parent component which is the QueueWebSocket to my other component which is wrapped by my parent component.
Here is my parent component:
import { useEffect, useState, createContext } from 'react';
import { getDjangoWebsocketHost } from 'components/global/function/env';
import { Typography } from '#material-ui/core';
// initialize websocket context
const QueueContext = createContext(null);
// export websocket context
export { QueueContext }
// initialize handler for websocket connection
// before DOM is ready
let wsInit = null;
const QueueWebsocket = (props) => {
const { children } = props;
// websocket payload
const [responsePayload, setResponsePayload] = useState(null);
// handler for websocket connection when DOM is ready
const [ws, setWs] = useState(null);
// state for checking if websocket connection has been established
const [wsReady, setWsReady] = useState(false);
// state for holding data received from the websocket connection
// this is sent across all logged in PGAN users when data is available
// const [globalNotification, setGlobalNotification] = useState(payload);
// function for sending CONNECT event
const connectEvent = ({ requestPayload }) => {
if (wsReady) {
encodedRequestPayload = new Uint8Array(JSON.stringify(
requestPayload).split('').map(
c => c.charCodeAt(0)
)
).buffer;
ws.send(encodedRequestPayload);
}
};
// function for sending message via websocket connection
// const sendMessage = ({ requestPayload }) => {
// console.log(requestPayload)
// let encodedRequestPayload = new Uint8Array(JSON.stringify(
// requestPayload).split('').map(
// c => c.charCodeAt(0)
// )
// ).buffer;
// console.log(encodedRequestPayload)
// ws.send(encodedRequestPayload);
// };
// function for opening connection,
// monitoring closed connection,
// and monitoring incoming data from the websocket connection
const ws_connect = (s) => {
if (s) {
// set 'ws' and 'wsReady' states
// when websocket connection is opened
s.onopen = () => {
setWs(s);
setWsReady(true)
}
// if connection is closed,
// attempt to reconnect after 1 second
s.onclose = (e) => {
setTimeout(() => {
ws_connect();
}, 1000);
}
s.onmessage = (e) => {
// the data received from the websocket connection
let receivedData = e.data;
// the payload is in JSON,
// but our websocket server sends it as binary data
// so we need to parse it to be able for JS to understand
let binary_data = new Uint8Array(receivedData).reduce((p, c) => p + String.fromCharCode(c), '');
receivedData = JSON.parse(binary_data);
// assign the 'payload' object to 'data'
// this time, we now have the parsed value of the websocket message
receivedData = receivedData['payload'];
// deconstruct the payload
// expectedly, we expect the ff. as default:
// message -> any message from the websocket connection
// event -> name of the corresponding event from the websocket connection
// data -> any additional information
const { event, data } = receivedData;
// check what event was invoked
// and perform respective operation
switch(event) {
case "EVENT_CALL":
if(data) {
// console.log(data);
setResponsePayload((prev) => ({...prev, event , data}));
}
break;
default:
break;
}
}
}
}
// function for running ws_connect() function above,
// ONLY when the DOM is ready
const initializeConnection = () => {
if (typeof window !== 'undefined') {
wsInit = new WebSocket(
`${getDjangoWebsocketHost()}/ws/queuePage/test/`);
if (wsInit) {
wsInit.binaryType = 'arraybuffer';
ws_connect(wsInit);
}
}
};
// attempt to connect to 'ws/login/'
// when this component loads
useEffect(() => {
initializeConnection();
}, []);
return (
<QueueContext.Provider
value={{
responsePayload,
}}
>
{ children }
</QueueContext.Provider>
);
}
export default QueueWebsocket;
now i tried to wrapped my other component from the component above to get the responsePayload prop from my children component but it gives me null.
Here is what i tried:
import { useState, useCallback, useMemo, useRef, useContext } from 'react';
import { Row, Col, Divider } from 'antd';
import { makeStyles } from '#material-ui/core/styles';
import { useRouter } from 'next/router';
import QueueWebsocket from 'components/global/function/websocket/queue_service/QueueWebsocket';
import {QueueContext} from 'components/global/function/websocket/queue_service/QueueWebsocket';
const useStyles = makeStyles((theme) => ({
margin: {
margin: theme.spacing(1),
},
paper: {
textAlign: 'center',
color: theme.palette.text.secondary,
},
}));
const Test = (props) => {
const queueContext = useContext(QueueContext);
const classes = useStyles();
const router = useRouter();
const handle = useFullScreenHandle();
console.log(queueContext);
return (
<QueueWebsocket>
<Row>
{console.log(queueContext)}
</Row>
</QueueWebsocket>
)
}
export default Test;

React push Api response in the setState

in my EventForm i have this const, this is a dialog form
this is my EventForm.js
const EventForm = (props) => {
const { setOpenPopup, records, setRecords, setMessage, setOpenSnackbar } = props
const addEvent = () => {
axios.post('https://jsonplaceholder.typicode.com/events', (event)
.then(resp => {
console.log(resp.data)
const newData = [{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}]
setRecords([{ ...records, newData}])
//
setOpenPopup(false)
setMessage('New Event added')
setOpenSnackbar(true)
})
.catch([])
}
export default EventForm
EventForm.propTypes = {
setOpenPopup: PropTypes.func,
records: PropTypes.array,
setRecords: PropTypes.func,
setMessage: PropTypes.func,
setOpenSnackbar: PropTypes.func
}
}
in my EventTable.js
const [records, setRecords] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/events')
.then(resp => {
const newData = resp.data.map((item) => ({
title: item.name,
start: item.starts_at,
end: item.ends_at
}))
setRecords(newData)
})
.catch(resp => console.log(resp))
}, [])
fullcalendar...
events={records}
im trying to push the API post response to my setRecords. so when the dialog form close it will not use the GET response. ill just get the new record and render to my view
but im getting an error:
Unhanded Rejection (TypeError): setRecords is not a function
I suspect you are using React Hooks. Make sure that your records state looks like this
const [records, setRecords] = useState([]);
In your axios request, it looks like that you are trying to spread the values of records which is an array to an object. I'd suggest refactoring this to something like this. Instead of trying to spread an array into the object, take the previous state and merge it with the new one.
setRecords(prevRecords => [...prevRecords, ...newData])
Here's an example using React Hooks how the component could look like
import React from "react";
import axios from "axios";
const MyComponent = ({
setOpenPopup,
records,
setRecords,
setMessage,
setOpenSnackbar
}) => {
const addEvent = () => {
axios
.post("https://jsonplaceholder.typicode.com/events", event) // Make sure this is defined somewhere
.then((resp) => {
const { name, starts_at, ends_at } = resp.data;
const newData = [
{
title: name,
start: starts_at,
end: ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
return (
<div>
<button onClick={addEvent}>Click me </button>
</div>
);
};
export default MyComponent;
If you are not using React Hooks and use Class components, then make sure that you pass setRecords to your component in props. Plus, in your props destructuring, make sure you add this to the props, otherwise, it can lead to unwanted behaviour. Also, move your request function out of the render method and destructure values from the props that you need inside the function. I've also noticed that your axios syntax was incorrect (forgot to close after the event) so I fixed that as well. Here's an example of how you can improve it.
import React from "react";
import axios from "axios";
class MyComponent extends React.Component {
addEvent = () => {
const {
setOpenPopup,
setRecords,
setMessage,
setOpenSnackbar
} = this.props;
axios
.post("https://jsonplaceholder.typicode.com/events", event)
.then((resp) => {
console.log(resp.data);
const newData = [
{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
//
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
render() {
return (
<div>
<button onClick={() => this.addEvent()}>Click me</button>
</div>
);
}
}
export default MyComponent;

Issues triggering Modal to show inside useEffect hook [duplicate]

I get this error:
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
when fetching of data is started and component was unmounted, but function is trying to update state of unmounted component.
What is the best way to solve this?
CodePen example.
default function Test() {
const [notSeenAmount, setNotSeenAmount] = useState(false)
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [])
async function updateNotSeenAmount() {
let data // here i fetch data
setNotSeenAmount(data) // here is problem. If component was unmounted, i get error.
}
async function anotherFunction() {
updateNotSeenAmount() //it can trigger update too
}
return <button onClick={updateNotSeenAmount}>Push me</button> //update can be triggered manually
}
The easiest solution is to use a local variable that keeps track of whether the component is mounted or not. This is a common pattern with the class based approach. Here is an example that implement it with hooks:
function Example() {
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
let isCancelled = false;
simulateSlowNetworkRequest().then(() => {
if (!isCancelled) {
setText("done!");
}
});
return () => {
isCancelled = true;
};
}, []);
return <h2>{text}</h2>;
}
Here is an alternative with useRef (see below). Note that with a list of dependencies this solution won't work. The value of the ref will stay true after the first render. In that case the first solution is more appropriate.
function Example() {
const isCancelled = React.useRef(false);
const [text, setText] = React.useState("waiting...");
React.useEffect(() => {
fetch();
return () => {
isCancelled.current = true;
};
}, []);
function fetch() {
simulateSlowNetworkRequest().then(() => {
if (!isCancelled.current) {
setText("done!");
}
});
}
return <h2>{text}</h2>;
}
You can find more information about this pattern inside this article. Here is an issue inside the React project on GitHub that showcase this solution.
If you are fetching data from axios(using hooks) and the error still occurs, just wrap the setter inside the condition
let isRendered = useRef(false);
useEffect(() => {
isRendered = true;
axios
.get("/sample/api")
.then(res => {
if (isRendered) {
setState(res.data);
}
return null;
})
.catch(err => console.log(err));
return () => {
isRendered = false;
};
}, []);
TL;DR
Here is a CodeSandBox example
The other answers work of course, I just wanted to share a solution I came up with.
I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !
Installation :
npm install use-state-if-mounted
Usage :
const [count, setCount] = useStateIfMounted(0);
You can find more advanced documentation on the npm page of the hook.
Here is a simple solution for this. This warning is due to when we do some fetch request while that request is in the background (because some requests take some time.)and we navigate back from that screen then react cannot update the state. here is the example code for this. write this line before every state Update.
if(!isScreenMounted.current) return;
Here is Complete Example
import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
return () => isScreenMounted.current = false
},[])
const ConvertFileSubmit = () => {
if(!isScreenMounted.current) return;
setUpLoading(true)
var formdata = new FormData();
var file = {
uri: `file://${route.params.selectedfiles[0].uri}`,
type:`${route.params.selectedfiles[0].minetype}`,
name:`${route.params.selectedfiles[0].displayname}`,
};
formdata.append("file",file);
fetch(`${BASEURL}/UploadFile`, {
method: 'POST',
body: formdata,
redirect: 'manual'
}).then(response => response.json())
.then(result => {
if(!isScreenMounted.current) return;
setUpLoading(false)
}).catch(error => {
console.log('error', error)
});
}
return(
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Text>Search Screen</Text>
</ScrollView>
</SafeAreaView>
</>
)
}
export default SearchScreen;
const styles = StyleSheet.create({
scrollView: {
backgroundColor:"red",
},
container:{
flex:1,
justifyContent:"center",
alignItems:"center"
}
})
This answer is not related to the specific question but I got the same Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. and as a React newcomer could not find a solution to it.
My problem was related to useState in an unmounted component.
I noticed that I was calling a set state function (setIsLoading) after the function that unmounted my component:
const Login = () => {
const [isLoading, setIsLoading] = useState(false);
const handleLogin = () => {
setIsLoading(true);
firebase.auth().then(
functionToUnMountLoginSection();
// the problem is here
setIsLoading(false);
)
}
}
The correct way is to call setIsLoading when the component is still mounted, before calling the function to unmount/process user login in my specific case:
firebase.auth().then(
setIsLoading(false);
functionToUnMountLoginSection();
)
You add the state related datas into the useEffect body for not rerunning them every rerendering process. This method will solve the problem.
useEffect(() => {
let timer = setInterval(updateNotSeenAmount, 2000)
return () => clearInterval(timer)
}, [notSeenAmount])
REF: Tip: Optimizing Performance by Skipping Effects
Custom Hook Solution (ReactJs/NextJs)
Create a new folder named 'shared' and add two folders named 'hooks', 'utils' in it. Add a new file called 'commonFunctions.js' inside utils folder and add the code snippet below.
export const promisify = (fn) => {
return new Promise((resolve, reject) => {
fn
.then(response => resolve(response))
.catch(error => reject(error));
});
};
Add a new file called 'fetch-hook.js' inside hooks folder and add the code snippet below.
import { useCallback, useEffect, useRef } from "react";
import { promisify } from "../utils/commonFunctions";
export const useFetch = () => {
const isUnmounted = useRef(false);
useEffect(() => {
isUnmounted.current = false;
return () => {
isUnmounted.current = true;
};
}, []);
const call = useCallback((fn, onSuccess, onError = null) => {
promisify(fn).then(response => {
console.group('useFetch Hook response', response);
if (!isUnmounted.current) {
console.log('updating state..');
onSuccess(response.data);
}
else
console.log('aborted state update!');
console.groupEnd();
}).catch(error => {
console.log("useFetch Hook error", error);
if (!isUnmounted.current)
if (onError)
onError(error);
});
}, []);
return { call }
};
Folder Structure
Our custom hook is now ready. We use it in our component like below
const OurComponent = (props) => {
//..
const [subscriptions, setSubscriptions] = useState<any>([]);
//..
const { call } = useFetch();
// example method, change with your own
const getSubscriptions = useCallback(async () => {
call(
payment.companySubscriptions(userId), // example api call, change with your own
(data) => setSubscriptions(data),
);
}, [userId]);
//..
const updateSubscriptions = useCallback(async () => {
setTimeout(async () => {
await getSubscriptions();
}, 5000);// 5 seconds delay
}, [getSubscriptions]);
//..
}
In our component, we call 'updateSubscriptions' method. It will trigger 'getSubscriptions' method in which we used our custom hook. If we try to navigate to a different page after calling updateSubscriptions method before 5 seconds over, our custom hook will abort state update and prevent that warning on the title of this question
Wanna see opposite?
Change 'getSubscriptions' method with the one below
const getSubscriptions = useCallback(async () => {
const response = await payment.companySubscriptions(userId);
setSubscriptions(response);
}, [userId]);
Now try to call 'updateSubscriptions' method and navigate to a different page before 5 seconds over
Try this custom hook:
import { useEffect, useRef } from 'react';
export const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
function Example() {
const isMounted = useIsMounted();
const [text, setText] = useState();
const safeSetState = useCallback((callback, ...args) => {
if (isMounted.current) {
callback(...args);
}
}, []);
useEffect(() => {
safeSetState(setText, 'Hello')
});
}, []);
return <h2>{text}</h2>;
}

Categories