Reputation: 47
I am a noob with React functional components and this is my first try with socket-io.
I have a functional component which is an inbox that renders up to ten "alert" rows per page from an InboxList
array. I am using socket-io so that multiple clients can see in real-time whenever a specific row is clicked (the row turns grey).
The problem is that most times the client (observing the row change, not clicking) will have their inbox empty (0 alerts in InboxList) once an alert is clicked.
After some debugging, I found that as soon as the socket.on('alertClicked') is triggered, my inboxList state is empty (instead of having the same 10 alerts I am expecting).
This will cause my inboxListCopy
inside greyOutAlert()
to of course be empty.
If anyone has suggestions that would be greatly appreciated.
Functional component code for context:
import React, { useState, useContext, useEffect } from 'react';
import io from 'socket.io-client';
import Axios from 'axios';
import Table from 'react-bootstrap/Table';
import Pagination from 'react-responsive-pagination';
import { useHistory } from 'react-router-dom';
import '../../App.css';
let socket;
export default function Inbox() {
const [token, setToken] = useState(() => {
return localStorage.getItem('auth-token');
});
const [currentPage, setCurrentPage] = useState(() => {
return 1;
});
const [alertCount, setAlertCount] = useState(() => {
return 5;
});
const [inboxList, setInboxList] = useState(() => {
return [];
});
const history = useHistory();
useEffect(() => {
const connectionOptions = {
"force new connection" : true,
"reconnectionAttempts": "Infinity",
"timeout" : 10000,
"transports" : ["websocket"]
}
socket = io('http://localhost:5000', connectionOptions);
return () => {
socket.emit('disconnected');
socket.off();
}
}, []);
useEffect(()=>{
fetchAlertCount();
},[])
useEffect(()=>{
socket.on('alertClicked', (alertId) => {
greyOutAlert(alertId);
});
},[])
const greyOutAlert = (alertId) => {
let inboxListCopy = [...inboxList];
for (let i = 0; i < inboxListCopy.length; i++) {
if (inboxListCopy[i]._id === alertId) {
inboxListCopy[i].hasBeenReviewed = true;
break;
}
}
setInboxList(inboxListCopy);
};
useEffect(() => {
fetchAlerts();
},[currentPage]);
const fetchAlertCount = async () => {
const config = {
headers: {
'x-auth-token': token,
}
};
const alertCountResponse = await Axios.get('http://localhost:5000/alertinbox/totalAlerts', config);
setAlertCount(alertCountResponse.data.count);
};
const fetchAlerts = async () => {
const config = {
headers: {
'x-auth-token': token,
'page': currentPage
}
};
const alertsResponse = await Axios.get('http://localhost:5000/alertinbox/', config);
setInboxList(alertsResponse.data.alerts);
};
const handleClick = (alertId, messageType) => {
socket.emit('clientClickedAlert', {alertId});
switch (messageType) {
case '2+ Minutes':
history.push(`/2plusminutes/${alertId}`, {alertId});
break;
case 'SOS':
history.push(`/sos/${alertId}`, {alertId});
break;
case 'Low Battery':
history.push(`/lowbattery/${alertId}`, {alertId});
break;
};
};
return (
<React.Fragment>
<div id='inbox'>
<Table>
<thead>
<tr>
<th>Alert Type</th>
<th>Customer Name</th>
<th>Timestamp</th>
<th>Vehicle</th>
</tr>
</thead>
<tbody>
{inboxList.map(function(alert, index){
return(
<tr key={index} onClick={() => handleClick(alert._id, alert.messageType)} className={alert.hasBeenReviewed ? 'darken' : 'lighten'}>
<td>{alert.messageType}</td>
<td>{alert.ownerName}</td>
<td>{alert.createdAt}</td>
<td>{alert.vehicle.year + ' ' + alert.vehicle.make + ' ' + alert.vehicle.model}</td>
</tr>
)
})}
</tbody>
</Table>
</div>
<Pagination
current={currentPage}
total={Math.ceil(alertCount/10)}
onPageChange={setCurrentPage}
maxWidth={100}
/>
</React.Fragment>
)
}
Upvotes: 1
Views: 516
Reputation: 47
Okay I figured it out. If you ever need to update the state of something, and that new state depends on the previous state, make sure to use functional updates.
In my code, I needed the current inboxList state (which is actually the previous state since useEffect() is run after a render) to compute the new inboxList state. To achieve this, I used the optional callback that is part of React's useState hook.
Correct code (useEffect() method updated):
useEffect(()=>{
socket.on('alertClicked', (alertId) => {
setInboxList(inboxList => {
let inboxListCopy = [...inboxList];
for (let i = 0; i < inboxListCopy.length; i++) {
if (inboxListCopy[i]._id === alertId) {
inboxListCopy[i].hasBeenReviewed = true;
break;
}
}
return inboxListCopy;
});
});
},[])
Upvotes: 1
Reputation: 23
I'll suggest you pass the exact initial state to the useState hook instead of a function like below:
const [inboxList, setInboxList] = useState([]);
Upvotes: 0