How to use Socket.IO in useEffect() statement? - reactjs

I cam across a strange thing which I don't quite get it.
I have a listener on event newOrder which should be received every time when a newOrder is emitted from backend. The problem here is that if I use it like this useEffect(() => { ... }, []) it will fire only once which somehow make sense but if I use it like useEffect(() => { ... }) it fires too many times because of the new renders.
My question is how is best to use it in a useEffect to listen every time when a newOrder is received, or is there a better approach?
Here is my code
useEffect(() => {
// WAIT FOR A NEW ORDER
ws.on('newOrder').then(data => {
dispatch(addOrder(data));
dispatch(updateStatus('received'));
});
// CLIENT CANCELED
ws.on('clientCanceledOrder').then(() => {
dispatch(updateStatus('ready'));
dispatch(clearPrice());
alert('Your rider has canceled the order!');
});
return () => {
ws.off('newOrder');
ws.off('clientCanceledOrder');
};
}, []);
Here is my SocketClient.js
export default class socketAPI {
socket;
connect(user) {
this.socket = io(BASE_URL, {
query: `type=driver&id=${user}`,
reconnection: true,
transports: ['websocket', 'polling'],
rememberUpgrade: true,
});
return new Promise((resolve, reject) => {
this.socket.on('connect', () => {
console.log(this.socket.io.engine.id);
resolve(true);
});
this.socket.on('connect_error', error => reject(error));
});
}
disconnect() {
return new Promise(resolve => {
this.socket.disconnect(reason => {
this.socket = null;
resolve(false);
});
});
}
emit(event, data) {
return new Promise((resolve, reject) => {
if (!this.socket) {
return reject('No socket connection. Emit Event');
}
return this.socket.emit(event, data, response => {
// Response is the optional callback that you can use with socket.io in every request.
if (response.error) {
return reject(response.error);
}
return resolve(response);
});
});
}
on(event) {
// No promise is needed here.
return new Promise((resolve, reject) => {
if (!this.socket) {
return reject('No socket connection. On event');
}
this.socket.on(event, data => {
resolve(data);
});
});
}
off(event) {
// No promise is needed here.
return new Promise((resolve, reject) => {
if (!this.socket) {
return reject('No socket connection. On event');
}
this.socket.off(event, data => {
resolve(data);
});
});
}
}
then I call this const socketClient = new SocketClient(); and this
`useEffect(() => {
(async () => {
const isConnected = await socketClient.connect(driver.id);
dispatch(updateSocketStatus(isConnected));
})();
}, [driver]);` inside my context

In general promise is resolved/reject only one time and then no more. But socket.io can emit more than one message. I suggest you to rework WS wrapper method to add event listener instead of promise
on(event, listener) {
if (!this.socket) {
throw 'No socket connection. On event';
}
this.socket.on(event, listener);
}

Related

SignalR connect setup in react- using useEffects

I'm using "#microsoft/signalr": "^6.0.5", and trying to set up a connection.
It is able to connect with the backend, but I am not sure if my setup looks OK for when the connection fails.
Specifically, I am wondering if the last useEffect is correctly written (the placement of the onClose clause)
useEffect(() => {
const newConnection = new HubConnectionBuilder()
.withUrl(
"https://localhost:3000/workorderHub",
{ accessTokenFactory: () => token, withCredentials: false }
)
.configureLogging(LogLevel.Information)
.withAutomaticReconnect()
.build();
setConnection(newConnection);
}, []);
useEffect(() => {
async function start() {
if (connection) {
try {
connection
.start()
.then(() => {
connection.invoke("SubscribeToProject", projectId); // calling hub method from the client
})
.catch((err) => {
console.error(err.toString());
});
connection.on(
"OperationUpdated",
(projectId, operationId, operation) => {
// function called from the backend Hub
actions.updateSyncedOperation({ operationId, operation });
}
);
} catch (err) {
console.log({ err });
setTimeout(start, 5000);
}
} else {
connection.onclose(async () => {
await start();
});
}
}
start();
}, [connection]);
For React 18 with the new strictMode behaviour i do the following.
It only creates one connection without any errors and it seems to cleanup properly during strictmode behaviour.
export const useLiveUpdates = () => {
const [connectionRef, setConnection] = useState < HubConnection > ();
function createHubConnection() {
const con = new HubConnectionBuilder()
.withUrl(`${EnvService.getUrlHub('url')}`, {
accessTokenFactory: () => AuthService.getToken(),
})
.withAutomaticReconnect()
.build();
setConnection(con);
}
useEffect(() => {
createHubConnection();
}, []);
useEffect(() => {
if (connectionRef) {
try {
connectionRef
.start()
.then(() => {
connectionRef.on('message', () => { ...
});
})
.catch((err) => {
logger.error(`Error: ${err}`);
});
} catch (error) {
logger.error(error as Error);
}
}
return () => {
connectionRef ? .stop();
};
}, [connectionRef]);
};

Test that errors are thrown in use Effect hook

I have a component that fetches data wrapped in a function to made async calls cancelable:
useEffect(() => {
const asyncRequest = makeCancelable(myService.asyncRequest());
asyncRequest.promise
.then((result) =>
setState(result),
)
.catch((e) => {
if (!e?.isCanceled) {
//Case the rejection is not caused by a cancel request
throw e;
}
});
return () => {
asyncRequest.cancel();
};
},[])
I want to test that, when the rejection is not coming from a cancel request, the error is re-thrown (I'm filtering out cancel rejections since they are not true errors). So the goal is intercept exceptions coming from useEffect
How can I test it with enzyme and/or jest?
it('should not filter rejection not caused by cancel', () => {
let promise = Promise.reject(new Error('Generic error'));
when(myService.asyncRequest()).thenReturn(promise); // This will cause useEffect to throw
const myComponent = mount(<MyComponent />) // How to intercept the error?
})
To give further context here is the code of makeCancelable:
export function makeCancelable<T>(promise: Promise<T>): CancelablePromise<T> {
let isCanceled = false;
const wrappedPromise = new Promise<T>((resolve, reject) => {
promise.then(
(val) => (isCanceled ? reject({ isCanceled: true }) : resolve(val)),
(error) => (isCanceled ? reject({ isCanceled: true }) : reject(error)),
);
});
return {
promise: wrappedPromise,
cancel() {
isCanceled = true;
},
};
}

Message Collector not worth

I'm making a support command: you type a command, the bot send you a message and then you reply to that message. I've used the awaitMessages function but it doesn't work.
case `support` : {
message.channel.send("What's your problem?");
let filter = m => m.author.id === message.author.id;
let msg = await message.channel.awaitMessages(filter, {maxMatches: 1});
message.channel.send("Your problem is: " + msg.first().content);
break;
}
To use .then() you need to return a Promise. This is a basic example of how you can use Promise:
const myFunction = () => {
return new Promise((resolve, reject) => {
if(taskIsSuccesFullyDone)
{
resolve(true); // Pass anything
}else{
reject('Something went wrong!');
}
});
}
myFunction().then(() => {
// Task is succesful completed.
// Do anything
})
.catch(error => console.log(error.message || error));
In your case, your code would look something like this:
function support_message() {
return new Promise((resolve, reject) => {
message.author.send(`Hello, <#${message.author.id}>, reply to this message explaining the problem you have.`)
.then(message => resolve(message))
.catch((error) => {
message.reply("I can't send you messages, be sure that you allow direct messages from unknown users to use this command.");
reject(error);
})
});
}
case `staff-support` : {
support_message().then(message => {
// We got the message object passed from the resolved Promise
// Do anything here
})
.catch((err) => {
// There was a problem!
// Do anything here.
});
break;
}

need to know when all async operation completed

I have the following code:
callAPI() {
return this.myModel.map(action(retValue => {
return this.myApi.doWork(retValue.Input).then(action(model => {
this.model.Input = Object.assign({}, model);
this.saveState();
})).catch(error => {
throw(error);
});
}));
if my client code I am doing something like this:
myStore.callAPI().then(() => {
console.log("completed");
this.setState({ loading: false });
});
I am getting an error
.then() is not a function
I want to get a callback when all async operations are completed.
Please use Promise.all to await multiple promises and return the result which is another Promise which you can call .then on.
In order to always return a promise after an imperative block of code you can return Promise.resolve() that will make the code chainable.
callAPI() {
return Promise.all(
this.myModel.map(action(retValue => (
this.myApi.doWork(retValue.Input).then(action(model => {
this.model.Input = Object.assign({}, model);
this.saveState();
return Promise.resolve();
}))
)));
);
}
Please see an example:
const sleep = timeout => new Promise(resolve =>
setTimeout(() => resolve(timeout), timeout)
);
const tap = timeout => {
console.log(`Task completed after ${timeout}ms`);
return timeout;
}
const tasks = [
sleep(1000),
sleep(2000),
sleep(500),
sleep(30),
sleep(2500),
sleep(450)
].map(task => task.then(tap));
Promise.all(tasks).then(x => {
console.log(x);
});

reactJS how to stop it listening to ajax request

I have ajax call in componentdidmount. And and then setState inside the ajax promise.
The code is like this
componentDidMount(){
axios.post('mydomian.com/item/',this.state)
.then(function (response) {
const res = response.data
if (res.status === 'OK') {
this.setState({items :res.list})
}else{
console.log('can not load data', response)
}
}.bind(this))
}
componentWillUnmount(){
how to stop everything about axios?
}
This causes error 'can not setstate on an unmounted component', when I navigate to other route.
So I think what I should do is remove axios listener in the componentwillunmount. How to would you do it?
A very simple solution could be to set a flag on unmount and utilize it within the promise resolution, like so:
componentDidMount(){
axios.post('mydomian.com/item/',this.state)
.then(function (response) {
if (this.unmounted) return;
const res = response.data
if (res.status === 'OK') {
this.setState({items :res.list})
}else{
console.log('can not load data', response)
}
}.bind(this))
}
componentWillUnmount(){
this.unmounted = true;
}
I have find a great solution that has been defined by istarkov
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
How to use:
const somePromise = new Promise(r => setTimeout(r, 1000));
const cancelable = makeCancelable(somePromise);
cancelable
.promise
.then(() => console.log('resolved'))
.catch(({isCanceled, ...error}) => console.log('isCanceled', isCanceled));
// Cancel promise
cancelable.cancel();
the solution has been found there.
My implementation.
Inside my function
const promiseShareByEmail = makeCancelable(this.props.requestShareByEmail(obj.email, obj.url));
promiseShareByEmail.promise.then(response => {
const res = response.data;
if (res.code != 0)
throw new Error(res.message);
this.setState({
message: {
text: TextMeasurements.en.common.success_share_test,
code: Constants.ALERT_CODE_SUCCESS
}
});
}).catch(err => {
if (err.isCanceled)
return;
this.setState({
message: {
text: err.message,
code: Constants.ALERT_CODE_ERROR
}
})
});
this.promiseShareByEmail = promiseShareByEmail;
this.props.requestShareByEmail(obj.email, obj.url) that function returns promise from axios.
when component will unmount cancel function should be invoked.
componentWillUnmount() {
this.promiseShareByEmail.cancel();
}
enjoy.

Resources