I am working on an app that needs to count the number of users that are viewing a webpage at the same time. To do that, I created a React component that is responsible for the page display, and every time when it loads it sends a request to the Node.JS backend whose sole purpose is to keep track of the amount of users. Every time the page loads, it sends the get request to Node to get the amount of users and displays it as a state, and every time the user leaves the page the event listener must send a post request to decrement the value of variable by one. This is what I have so far:
server.js (back-end)
//This is a small backend that keeps track of how many people are on each page.
const express = require('express')
const cors = require('cors')
const app = express()
const whiteList = ['http://localhost:3000', 'http://127.0.0.1:3000']
const corsOptions = {
origin: function (origin, callback) {
if (!origin || whiteList.indexOf(origin) !== -1)
callback(null, true)
else
callback(new Error("Not allowed by CORS"))
},
credentials: true,
}
app.use(cors(corsOptions))
let googleCounter = 0, discordCounter = 0, linuxCounter = 0
console.log(googleCounter, discordCounter, linuxCounter)
app.get("/google", (request, response) => {
console.log(googleCounter, discordCounter, linuxCounter)
console.log(`Google counter ${googleCounter} increased to ${googleCounter + 1}`)
googleCounter++
response.send(`Google Counter: ${googleCounter}`)
})
app.get("/discord", (request, response) => {
discordCounter++
response.send(discordCounter)
})
app.get("/linux", (request, response) => {
linuxCounter++
response.send(linuxCounter)
})
//These methods will fire when users leave the pages.
app.post("/google", () => {
console.log(`Google counter ${googleCounter} decreased to ${googleCounter - 1}`)
googleCounter--})
app.post("/discord", () => discordCounter--)
app.post("/linux", () => linuxCounter--)
app.listen(5000, () => console.log(`Server started on port 5000: ${googleCounter}, ${discordCounter}, ${linuxCounter}`))
Google.js (the front-end)
//This is a generic page to view. Source: https://uk.wikipedia.org/wiki/Google
import React, {useState, useEffect} from "react"
import Base from "./Base"
export default function Google(props) {
const url = "http://localhost:5000/google"
const [counter, setCounter] = useState(1)
//Increment user counter when the page is loaded:
useEffect(() => {
if (!props.user) return;
fetch(url, {method: "GET"})
.then(response => {return response.text()})
.then(response => console.log(response))
.catch(error => console.log(error))
//Decrement user counter when the user leaves the page:
window.addEventListener("unload", () => fetch(url, {method: "POST"})
.then(response => {return response.text()})
.then(response => console.log(response))
.then(() => console.log("User left the page")))
.catch(error => console.log(error))
}, [props.user])
if (!props.user) return (<div><Base/></div>)
return (<div>
<h1>Google</h1>
<h2>Currently active users: {counter}</h2>
</div>)
}
My issue is that the googleCounter variable behaves unpredictably: when I run the server, it immediately falls to -3 to -5, and every time I send the get request it increments the variable but also decrements. I suspect the unload event could run more often than when user leaves the page, useEffect could have run more often or the get request could overlap with the post request in some way. When I tried to log the post request from the client-side, I've got the event listener is undefined:
How can I make sure that the googleCounter variable only decreases when the user leaves the page?
Related
I'm trying to create a custom notification counter, which shows the number of notifications that are unread.
I receive the notification content from the backend as an array.
My idea is to get the first value, check if the previous length is different from the current length, and add to this counter +1(or more).
useEffect(() => {
axios
.get(USER_API)
.then((response) => {
setNotificationList(response.data.data);
})
.catch((error) => console.log('error from getNotifications', error));
});
How can I make it work?
As right way could be that you place a key isRead in the backend.
and setThis key to true via api when user click on the notification.
and count the length of those items who has isRead false and set it to counter.
this way you will not be dependent on local state and this will work even if app is killed or uninstalled.
useEffect(() => {
axios
.get(USER_API)
.then((response) => {
const data = response.data?.data;
const unReadItems = data.filter((item)=> !item.isRead)
setUnReadCount(unReadItems.length)
setNotificationList(data);
})
.catch((error) => console.log('error from getNotifications', error));
});
use the callback function in set state
setNotificationList((prev) => {
if(prev?.length === response.data.data.length) return prev
return response.data.data
} );
I have this function to fetch data from another server(it basically send a command to run a python program, which writes an output into a text file on the remote machine and then it returns the content of the txt file)
const getServerStatus = () => { // returns result of of screen -ls
const commandQuery = "python3 /home/user/scripts/getServerStatus.py && cat /home/user/scripts/status.txt";
return fetch('http://10.0.0.253:3005/fetch', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
command: commandQuery
})
})
.then(res => {
return res.json();
})
.then(data => {
return data.status;
})
.catch(err => { // it catches when fatch fails (server is off)
return 'OFF';
});
};
And the function gets executed when the user visits home page
app.get('/', checkAuthenticated, async (req, res) => {
const status = await getServerStatus()
.then(data => {
return data;
});
res.render('index.ejs', {
name: req.user.username,
title: 'home',
server_status: status
});
})
It runs no problem and it returns data and the home page gets rendered very quickly, but the problem is that if I don't refresh the webpage for a curtain amount of time (maybe like 2-3 minutes or so), it'll take around 8-10 seconds to fetch and render the homepage again. I just can't figure out what the problem is. I think there is something wrong with my async functions but I am not sure. Or maybe my node server goes like an idle mode when there's no oncomming connections?
my /fetch endpoint
app.post('/fetch', (req, res) => {
exec(req.body.command,
function (error, stdout, stderr) {
res.send(JSON.stringify({status :stdout.replace(/\s/g, '')}));
console.log('[+] SERVER STATUS SENT');
if (error !== null) {
console.log('exec error: ' + error);
}
});
});
Turned out that it is a hardware issue? of my raspberry pi 4. If I run the node project on a different raspberry pi, it doesn't do that and runs perfectly without issues.
I'm currently working on a search functionality in React Native using axios.
When implementing search functionality i'm using debounce from lodash to limit the amount of requests sent.
However, since request responses are not received in same order there is a possibility of displaying incorrect search results.
For example when the user input 'Home deco' in input field there will be two requests.
One request with 'Home' and next with 'Home deco' as search query text.
If request with 'Home' takes more time to return than second request we will end up displaying results for 'Home' query text not 'Home deco'
Both results should be displayed to the user sequentially, if responses are returned in order but if 'Home' request is returned after 'Home deco' request then 'Home' response should be ignored.
Following is a example code
function Search (){
const [results, setResults] = useState([]);
const [searchText, setSearchText] = useState('');
useEffect(() => {
getSearchResultsDebounce(searchText);
}, [searchText]);
const getSearchResultsDebounce = useCallback(
_.debounce(searchText => {
getSearchResults(searchText)
}, 1000),
[]
);
function getSearchResults(searchText) {
const urlWithParams = getUrlWithParams(url, searchText);
axios.get(urlWithParams, { headers: config.headers })
.then(response => {
if (response.status === 200 && response.data)
{
setResults(response.data);
} else{
//Handle error
}
})
.catch(error => {
//Handle error
});
}
return (
<View>
<SearchComponent onTextChange={setSearchText}/>
<SearchResults results={results}/>
</View>
)
}
What is the best approach to resolve above issue?
If you want to avoid using external libraries to reduce package size, like axios-hooks, I think you would be best off using the CancelToken feature included in axios.
Using the CancelToken feature properly will also prevent any warnings from react about failing to cancel async tasks.
Axios has an excellent page explaining how to use the CancelToken feature here. I would recommend reading if you would like a better understanding of how it works and why it is useful.
Here is how I would implement the CancelToken feature in the example you gave:
OP clarified in the replies that they do not want to implement a cancelation feature, in that case I would go with a timestamp system like the following:
function Search () {
//change results to be a object with 2 properties, timestamp and value, timestamp being the time the request was issued, and value the most recent results
const [results, setResults] = useState({
timeStamp: 0,
value: [],
});
const [searchText, setSearchText] = useState('');
//create a ref which will be used to store the cancel token
const cancelToken = useRef();
//create a setSearchTextDebounced callback to debounce the search query
const setSearchTextDebounced = useCallback(
_.debounce((text) => {
setSearchText(text)
), [setSearchText]
);
//put the request inside of a useEffect hook with searchText as a dep
useEffect(() => {
//generate a timestamp at the time the request will be made
const requestTimeStamp = new Date().valueOf();
//create a new cancel token for this request, and store it inside the cancelToken ref
cancelToken.current = CancelToken.source();
//make the request
const urlWithParams = getUrlWithParams(url, searchText);
axios.get(urlWithParams, {
headers: config.headers,
//provide the cancel token in the axios request config
cancelToken: source.token
}).then(response => {
if (response.status === 200 && response.data) {
//when updating the results compare time stamps to check if this request's data is too old
setResults(currentState => {
//check if the currentState's timeStamp is newer, if so then dont update the state
if (currentState.timeStamp > requestTimeStamp) return currentState;
//if it is older then update the state
return {
timeStamp: requestTimeStamp,
value: request.data,
};
});
} else{
//Handle error
}
}).catch(error => {
//Handle error
});
//add a cleanup function which will cancel requests when the component unmounts
return () => {
if (cancelToken.current) cancelToken.current.cancel("Component Unmounted!");
};
}, [searchText]);
return (
<View>
{/* Use the setSearchTextDebounced function here instead of setSearchText. */}
<SearchComponent onTextChange={setSearchTextDebounced}/>
<SearchResults results={results.value}/>
</View>
);
}
As you can see, I also changed how the search itself gets debounced. I changed it where the searchText value itself is debounced and a useEffect hook with the search request is run when the searchText value changes. This way we can cancel previous request, run the new request, and cleanup on unmount in the same hook.
I modified my response to hopefully achieve what OP would like to happen while also including proper response cancelation on component unmount.
We can do something like this to achieve latest api response.
function search() {
...
const [timeStamp, setTimeStamp] = "";
...
function getSearchResults(searchText) {
//local variable will always have the timestamp when it was called
const reqTimeStamp = new Date().getTime();
//timestamp will update everytime the new function call has been made for searching. so will always have latest timestampe of last api call
setTimeStamp(reqTimeStamp)
axios.get(...)
.then(response => {
// so will compare reqTimeStamp with timeStamp(which is of latest api call) if matched then we have got latest api call response
if(reqTimeStamp === timeStamp) {
return result; // or do whatever you want with data
} else {
// timestamp did not match
return ;
}
})
}
}
I have a server backend written in Python with Flask-SocketIO. I'm utilizing it's room feature to make private conversations. Upon a join room event the server fires the following function to let the frontend know where to send messages to specific user:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient}, to=sid)
where sid is the private room created only for the user when connecting to a socket. Then I want to keep this information in React state in a map, like this:
function ChatWindow({ username, token }) {
const [responses, setResponses] = useState([]);
const [roomsMap, setRoomsMap] = useState(new Map());
const [currentRoom, setCurrentRoom] = useState("");
const [messageValue, setMessageValue] = useState("");
var socket = null;
useEffect(() => {
socket = socketIOClient(ENDPOINT);
});
useEffect(() => {
socket.on("global response", (data) => {
setResponses((responses) => [...responses, data]);
});
socket.on("room name response", (data) => {
console.log(`joined ${data.roomName} with ${data.recipient}`);
setCurrentRoom((currentRoom) => data.roomName);
setRoomsMap((roomsMap) => roomsMap.set(data.recipient, data.roomName));
});
return () => socket.close();
}, []);
const sendMessage = () => {
if (messageValue.length < 1) {
return;
}
socket.emit("global message", {
user_name: username,
message: messageValue,
timestamp: Date.now(),
});
setMessageValue("");
};
const joinRoom = (recipient) => {
socket.emit("join", {
token: token,
username: username,
recipient: recipient,
});
// setCurrentRoom(() => roomsMap.get(recipient));
};
const leaveRoom = (recipient) => {
socket.emit("leave", {
token: token,
username: username,
recipient: recipient,
});
const newRooms = roomsMap;
newRooms.delete(recipient);
console.log(`left room with ${recipient}`);
newRooms.forEach((val, key) => console.log(`${val}:${key}`));
setRoomsMap(newRooms);
};
const checkUser = (userToCheck) => {
if (userToCheck === username) {
return styles.chatFromUser;
} else {
return styles.chatToUser;
}
};
return (...);
}
export default ChatWindow;
Sadly, React doesnt react to the socket emitting message, even though it can be seen in network tab in developer tools. The global response works fine.
When I alter the backend function to:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient})
React suddenly works as expected. I'm trying to understand why it happens, especially when the browser seems to see the incoming messages as stated above, so it's most likely my bad coding or some React/Javascript thing.
Thank You for any help in advance.
The problem was that socket sometimes was created multiple times, therefore, the socket that useEffect was currently listening wasn't necessarily the one in the room. So I made one, global socket to fix this and whole thing now works.
so I wanted to dive into Socket.io and I am a complete beginner in it. I am a react developer and decided to test it with react, this is my client side:
import { io } from "socket.io-client";
function App() {
let [message, setMessage] = useState("");
let [messages, setMessages] = useState([]);
const socket = io("http://localhost:4000/", {
withCredentials: true,
cors: {
origin: "http://localhost:4000",
},
});
const sendMessage = (msg) => {
socket.emit("messageToServer", msg);
};
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
return (
<div className="App">
{messages}
<input
id="input"
autoComplete="off"
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={() => sendMessage(message)}>Send</button>
</div>
);
}
and this is my server side(Node, Express):
const app = require("express")();
const http = require("http").Server(app);
const io = require("socket.io")(http, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true,
},
});
io.on("connection", (socket) => {
let messages = [];
socket.on("messageToServer", (msg) => {
io.emit("messageToClient", msg);
console.log(msg);
});
});
http.listen(4000, () => {
console.log("listening on *:3000");
});
So basicly when I write something on the input and click the button, on the node server everything seems fine and the message logs only once, but when I open the tab that I send the message from and for example I typed 'First message' it will print something like: 'First messageFirst messageFirst messageFirst message' and so on...
But apparently, the duplication is happening only from the tab I am sending it from, because when I open a new one and test it, on the sender tab I get multiple like i said, but on the new one, I recieve it only one, just like it should be.
Thank you in advance!
The below snippet of code will keep App component being rendered multiple times, thus the same event handler for messageToClient will be registered multiple times and cause unexpected behavior in React.
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
You should wrap socket on event with useEffect, so that we can make sure nothing is duplicating.
useEffect(()=>{
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
// unbind the event handler when the component gets unmounted
return () => {
socket.off('messageToClient')
}
},[])