ReactJS: Why does navigating appear to change `readyState` of previous `EventSource`s? - reactjs

Question: Why does navigating appear to change the readyState of the previous EventSources?
===============================================
Explanation: I'm working on a frontend (React) in which the user can enter a sequence of search queries (i.e. strings) and for each search query, my backend (Flask) will return a sequence of URLs. For each search query, I've decided to receive the server's response via an EventSource. Specifically, I first create a React state array of backendEventSources:
const [backendEventSources, setBackendEventSources] = useState([]);
Then I update the backendEventSources when a new prompt comes in:
useEffect(() => {
console.log('Inside useEffect')
// Take 0 for the newest prompt.
const newBackendEventSource = new EventSource(
`https://localhost:8080/generate?counter=${promptsResultsArray[0].counter}&prompt=${promptsResultsArray[0].prompt}`,
{withCredentials: false})
newBackendEventSource.addEventListener('open', () => {
console.log('SSE opened!');
});
newBackendEventSource.addEventListener('error', (e) => {
console.log('SSE error!');
console.error('Error: ', e);
});
newBackendEventSource.addEventListener('close', (e) => {
console.log('SSE closed!');
const data = JSON.parse(e.data);
console.log("close data: ", data);
newBackendEventSource.close();
});
newBackendEventSource.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log("message data: ", data);
// https://stackoverflow.com/a/47580775/4570472
const newPromptsResultsArray = [...promptsResultsArray];
// Since we preprend new results, we need to compute the right index from
// the counter with the equation: length - counter - 1.
// e.g., For counter 2 of a length 3 array, we want index 0.
// e.g., For counter 2 of a length 4 array, we want index 1.
// Recall, the counter uses 0-based indexing.
const index = newPromptsResultsArray.length - data.counter - 1
newPromptsResultsArray[index].URIs = [data.uri];
newPromptsResultsArray[index].isLoading = false;
setPromptsResultsArray(newPromptsResultsArray);
// Instantiating the element and setting the src property starts preloading the image.
// for (const newImgURI of newImgURIs) {
// const imageElement = new Image();
// imageElement.src = newImgURI;
// }
// setTimeout(() => {setImgURIs(newImgURIs)}, 8000);
});
// Add new backend event source to state for persistence.
setBackendEventSources(backendEventSources => [
newBackendEventSource,
...backendEventSources])
return () => {
newBackendEventSource.close();
};
}, [prompt]);
I use URL params for React navigation:
const navigateToGenerateResults = (promptString) => {
console.log('Adding new prompt results to promptsResultsArray');
// State doesn't update immediately (or even synchronously). To ensure we can immediately
// access the new values, we create a newPromptsResults.
// https://stackoverflow.com/a/62900445/4570472
const newPromptsResults = {
isLoading: true,
prompt: promptString,
counter: promptsResultsArray.length,
URIs: ["https://simtooreal-public.s3.amazonaws.com/white_background.png"]
}
// Prepend the new prompt to our promptsResultsArray
// https://stackoverflow.com/a/60792858/4570472
setPromptsResultsArray(promptsResultsArray => [
newPromptsResults, ...promptsResultsArray])
console.log('Setting prompt to: ' + newPromptsResults.prompt)
setPrompt(newPromptsResults.prompt)
console.log('Navigating from /generate to /generate with prompt: ' + newPromptsResults.prompt)
navigate(`/generate?counter=${newPromptsResults.counter}&prompt=${newPromptsResults.prompt}`)
}
However, I've discovered that as soon as I navigate from one URL to another, the previous EventSource's ready state switches from 0/1 to 2. Additionally, my newBackendEventSource.addEventListener('close' function is never triggered.
Why does navigating appear to change the readyState of the previous EventSources?

Related

React store API response in localStorage

I have an API that returns a list which is shown as a grid in my main component. I store the list in localStorage bcz I don't want that API to be called every time the component is rendered. It looks something like this:
const setOldData = (dataRN) => {
console.log("Using old data...")
setContent(JSON.parse(dataRN));
let tmpArr = JSON.parse(dataRN).CodeList;
setData(JSON.parse(dataRN).RNList);
var tmp = groupBy(tmpArr, 'groupname');
setAktNaloga(tmp.GetListaAktivnostNaloga);
setTipAkcije(tmp.GetListaVrstaAkcijeNaloga);
setKorGrupa(tmp.GetListaKorisnickihGrupa);
setLinijaProdukta(tmp.GetListaLinijeProdukataNaloga);
setPrioritet(tmp.GetListaPrioritetNaloga);
setStanje(tmp.GetListaStanjeNaloga);
setStatus(tmp.GetListaStatusaNaloga);
setLokacija(tmp.GetListaPodrucjeOdrzavanja);
}
useEffect(() => {
showLoader();
console.log("Getting srv data");
window.addEventListener('resize', handleResize);
let unmounted = false;
let source = axios.CancelToken.source();
let datestring = dateStart.getDate() + "." + (dateStart.getMonth() + 1) + "." + dateStart.getFullYear();
console.log(datestring);
setDateStartSend(datestring);
let datestringEnd = dateEnd.getDate() + "." + (dateEnd.getMonth() + 1) + "." + dateEnd.getFullYear();
console.log(datestringEnd);
setDateEndSend(datestringEnd);
if (window.localStorage !== undefined) {
const dataRN = window.localStorage.getItem('rnList');
dataRN !== null ? setOldData(dataRN) : RNService.getRNList(source.token, datestring, datestringEnd, currentUser.userName).then(
(response) => {
if (!unmounted) {
localStorage.setItem('rnList', JSON.stringify(response.data));
setContent(response.data);
console.log('getRNLista', response.data);
let tmpArr = response.data.CodeList;
setData(response.data.RNList);
var tmp = groupBy(tmpArr, 'groupname');
console.log("Statusi; ");
console.log(tmp.GetListaStatusaNaloga);
setTipZahtjeva(tmp.GetListaVrstaZahtjeva);
console.log(tmp.GetListaVrstaZahtjeva);
console.log("AAA");
setAktNaloga(tmp.GetListaAktivnostNaloga);
setTipAkcije(tmp.GetListaVrstaAkcijeNaloga);
setKorGrupa(tmp.GetListaKorisnickihGrupa);
setLinijaProdukta(tmp.GetListaLinijeProdukataNaloga);
setPrioritet(tmp.GetListaPrioritetNaloga);
setStanje(tmp.GetListaStanjeNaloga);
setStatus(tmp.GetListaStatusaNaloga);
setLokacija(tmp.GetListaPodrucjeOdrzavanja);
console.log("Count: " + response.data.RNList.length);
hideLoader();
}
},
(error) => {
if (!unmounted) {
console.log("Error rq");
console.log(error.response.status);
if (error.response.status == 401) {
alert(error.response.data.message)
window.localStorage.removeItem('rnList')
logOut();
} else {
//alert(error.response.data.message)
alert("Problem");
}
}
}
);
}
hideLoader();
}, [reload]);
So, in useEffect it is checked if there is something in localStorage, if there is --> don't call the API; if there isn't --> call the API. Also, my login token lasts for an hour, so if you logged in now, in an hour, if you are in this component, useEffect would throw an error and kick you out for you to login again.
BUT, here comes the problem --> the error can't be thrown bcz once I filled up localStorage, I don't know how to empty it properly, or, what I want to say, I don't know WHERE exactly should I empty it.
I want to empty it once the token has ran out, but I don't know the best possible solution for it. Should I make an API that just checks the token validity and put it in setOldData method and, if it has ran out, throw an error, kick the user out and clear the localStorage?
Or, should I use sessionStorage, Redux...?
const currentTime = Date.now() / 1000; // to get in milliseconds
if (decoded.exp < currentTime) {
// Logout user
store.dispatch(logoutUser());
// Redirect to login
window.location.href = "./sign-in";
}
You may use Redux to manage the state of the application and like the above example you may check the expiration time and log out the user and clear the loaclstoreage
//Authaction.js -- redux
localStorage.removeItem("your-item");

Query Parameter Values Not Updating When Set from Google Map Component

I have a map component that contains some clickable overlays on the map. Users can click and unclick the overlays on the map to select them and when they do so the app loads some data based on the overlays that are currently selected.
The current structure is as follows:
User clicks the map which executes a function passed to the map as a prop, which takes the current value of the neighborhoods and either adds or removes them from the query string.
The function executes a history.push()
I use a useEffect checking the value of the query param neighborhood and send a request to the backend to fetch the listings if the values have changed.
My issue is that when the user clicks on the map, the function executes but the value pushed to the params is never updated, causing the logic to fail the next time the user clicks on the map.
Relevant Snippets of Code are as follows:
history/param variables:
const { region, state, neighborhood, transactiontype } = useParams();
const location = useLocation();
const { pathname, search } = location;
Function that is passed down to the child map component:
const updateChildComponentHandler = (dataFromChild, addorRemove) => {
SetLastSetter("map");
SetNeighborhoodTypeSelected("custom");
// if a new neighborhood is added, just taking the existing string and adding &`neighborhood`
if (addorRemove === "add") {
let newGroup = ""
if (neighborhood !== "any") {
newGroup = `${neighborhood}&${dataFromChild}`;
SetMapChangeType("add");
}
// if no neighborhood was initially selected, just replacing the "any" with the proper neighborhood
if (neighborhood === "any") {
SetMapChangeType("add");
newGroup = `${dataFromChild}`
}
// pushing the new parameter string
const newPath = pathname.replace(neighborhood, newGroup);
history.push(`${newPath}${search}`);
}
// same concept as above, just removing the neighborhood from the string if it is removed from the map
if (addorRemove === "remove") {
let newGroup;
if (neighborhood !== dataFromChild) {
newGroup = neighborhood.replace(`&${dataFromChild}`, "")
SetMapChangeType("delete");
}
if (neighborhood === dataFromChild) {
newGroup = "any";
SetMapChangeType("add");
}
if (neighborhood.split("&")[0] === dataFromChild && neighborhood !== dataFromChild) {
newGroup = neighborhood.replace(`${dataFromChild}&`, "")
SetMapChangeType("delete");
}
const newPath = pathname.replace(neighborhood, newGroup);
const newerPath = `${newPath}${search}`;
history.push(newerPath);
deleteListings(dataFromChild);
}
SetUpdateMap(true);
}
UseEffect Logic:
useEffect(() => {
const func = async () => {
let neighborhoodParams;
if (neighborhood !== "any") {
neighborhoodParams = neighborhood.replace("%20", " ").split("&")
}
if (neighborhood === "any") {
neighborhoodParams = [];
neighborhoodParams[0] = "any";
}
const neighborhoods = neighborhood.split("%20").join("_");
SetNeighborhoodParams(neighborhoodParams);
SetNeighborhoodParamString(neighborhoods);
if (mapChangeType === "add") {
if (neighborhoodParams.length > 0) {
if (!requestMulti) {
await fetchProperties(transactiontype, neighborhoodParams[neighborhoodParams.length - 1].split(" ").join("_"), filteredRegion, state, filters, "single")
}
if (requestMulti) {
await fetchProperties(transactiontype, neighborhoods, filteredRegion, state, filters, "multi")
SetRequestMulti(false);
}
}
}
}
func();
}, [neighborhood]);
The issue I am experiencing is that when the map initially loads, the neighborhood is set to "any". The first time a user clicks an overlay for a neighborhood, the correct data is sent from the map and the map/data requests update and the URL parameter up top shows the new neighborhood. However, the second time a user clicks, the value of { neighborhood } is not updated and is still set to "any", so the function just replaces the value of { neighborhood } rather than adding it on as per above. I previously coded this with class components and am trying to convert it to hooks, but it seems like there is some dissonance that is causing the map component not to have access to the updated history variable. I am new to react/hooks, and appreciate if anyone could lend some advice.
Thanks!

Restful API failing sporadically but "catch" statement is always called, even when it does not fail

We are using a restful API to retrieve information about esports matches being played. From time to time the page simply loads, with no info being returned form the API.
I am fairly confident that the issue is with the API itself, but wanted to double-check that we are not doing anything wrong. Please see our code below:
const proxyurl = "https://cors-anywhere.herokuapp.com/";
const url = "http://datafeed.bet/en/esports.json ";
fetch(proxyurl + url)
.then(response => response.json())
.then(data => {
const list = data;
const games =
list &&
list.Sport &&
list.Sport.Events &&
list.Sport.Events.map((match) =>
match.Name.substr(0, match.Name.indexOf(","))
);
const uniqueGames = [...new Set(games)];
let combinedMatches = [];
data &&
data.Sport &&
data.Sport.Events &&
data.Sport.Events.map((game) => {
game.Matches.map((match) => {
match.Logo = game.Logo;
match.TournamentName = game.Name;
match.CategoryID = game.CategoryID;
match.ID = game.ID;
});
combinedMatches = combinedMatches.concat(game.Matches);
});
this.setState({
gameData: data,
games: games,
uniqueGames: uniqueGames,
preloading: false,
filteredGames: combinedMatches,
allMatches: combinedMatches,
count: Math.ceil(combinedMatches.length / this.state.pageSize),
});
var i;
let allMatches = 0;
let temp;
for (i = 0; i < this.state.filteredGames.length; i++) {
temp = allMatches =
allMatches + this.state.filteredGames[i].Matches.length;
}
this.setState({ allMatches: allMatches });
})
.catch(console.log('error');
Something that confuses me is that whether the data is returned or not, the "catch" statement gets called, outputting "error" to the console. I would like to build in some workaround for when the data is not returned. Would this be placed in the "catch" statement? If so, why how do I only let the catch run if the operation actually fails.
When you do this:
.catch(console.log('error'))
You immediately invoke console.log('error') and pass its result (which is undefined) to the catch. In this case it's invoking it right away, before the AJAX operation is even performed.
What you want to pass to catch is a function which would invoke that operation if/when it needs to:
.catch(e => console.log('error'))
As an aside, you'd probably also want to log the error itself so you can see what happened, as opposed to just the string 'error':
.catch(e => console.log('error', e))

Nested callback function (React native JS )

I have a couple of different functions that helps set different states of the component and i wish to run the functions in order of one another. I know that there are multiple posts on these but they seem to be mostly catered for running one function after the other but i have more than two functions that i need to execute in order
Desired order
1) Set state of starting and final destination
2) Run this.getDirections() (This function sets the state of arrOfPolylines which i desire to reset through resetRouteSelectionStatus())
3) Run resetRouteSelectionStatus()
4) After running these functions i wish to have an empty this.state.arrOfPolylines
Actual results
There is no error in the code but it isnt entering the resetRouteSelectionStatus() as none of the console log are printed. Can someone please guide me on the right path?
<Button
onPress={() => { //on button press set final destination and starting location
{
(this.state.tempDestination.longitude != null && this.state.tempStarting.longitude != null) &&
this.setState({
finalDestination: {
latitude: this.state.tempDestination.latitude,
longitude: this.state.tempDestination.longitude,
},
startingLocation: {
latitude: this.state.tempStarting.latitude,
longitude: this.state.tempStarting.longitude,
}
}, () => {
this.getDirections((this.state.startingLocation.latitude + "," + this.state.startingLocation.longitude), (this.state.finalDestination.latitude + "," + this.state.finalDestination.longitude),
() => {this.resetRouteSelectionStatus()});
}
);
}
}}
title="Determine Directions"
color="#00B0FF"
resetRouteSelectionStatus() {
console.log('entered reset route selection status function')
this.setState({arrOfPolyline: null }, () => {console.log("i should be null nd come first" + this.state.arrOfPolyline)}) ;
this.setState({ selectChallengeStatus: null });
this.setState({ userRouteSelectionStatus: null }); //when user click on button set seleection status to 0 so route options will be displayed again after generation new route
//this.setState({arrOfDirectionDetails: []}); // clear past direction details when user select generate route with new starting/ending location
// clear past poly lines when user has selected new routes
//console.log("everything has been reset");
}
async getDirections(startLoc, destinationLoc) {
let resp = await fetch(`https://maps.googleapis.com/maps/api/directions/json?origin=${startLoc}&destination=${destinationLoc}&key="KEY"&mode=driving&alternatives=true`)
let respJson = await resp.json();
let routeDetails = respJson.routes;
let tempPolyLineArray = [];
let tempDirArrayRoute = [];
//console.log(startLoc);
//console.log(destinationLoc);
for (i = 0; i < routeDetails.length; i++) // respJson.routes = number of alternative routes available
{
let tempDirArray = []; // at every new route, initalize a temp direction array
let points = Polyline.decode(respJson.routes[i].overview_polyline.points);
let coords = points.map((point, index) => {
return {
latitude: point[0],
longitude: point[1]
}
})
tempPolyLineArray.push(coords);
for (k = 0; k < routeDetails[i].legs[0].steps.length; k++) // for each route go to the correct route, enter legs ( always 0), get the number of instruction for this route
{
//console.log (routeDetails[i].legs[0].steps[k])
tempDirArray.push(routeDetails[i].legs[0].steps[k]) // push all instructions into temp direction array
//this.state.arrOfDirectionDetails.push(routeDetails[i].legs[0].steps[k]); // for each instruction save it to array
}
tempDirArrayRoute.push(tempDirArray); // at the end of each route, push all instructions stored in temp array as an array into state
}
this.setState({ arrOfDirectionDetails: tempDirArrayRoute });
this.setState({ arrOfPolyline: tempPolyLineArray });
//creating my html tags
let data = [];
let temptitle = "Route ";
for (let j = 0; j < routeDetails.length; j++) {
data.push(
<View key={j}>
<Button
title={temptitle + j}
onPress={() => this.updateUser(j)}
/>
</View>
)
}
this.setState({ routebox: data })
}
So a few things to note here:
I would extract the inline function for the onButtonPress function into an async function
Something along the lines of:
const onButtonPressHandler = async () => {
... put your code here
}
Then you'll modify your onButtonPress to something like:
<Button
onPress={this.onButtonPressHandler}
... rest of your props here
/>
You can then properly use async / await in the buttonPress handler.
The setState function is not a synchronous function. If you rely on the results right away you'll be disappointed.
Each time you call setState you could trigger a rerender.
I would instead merge all of your setState calls into a single at the end.
The getDirections function does not include the callback, while it should be:
async getDirections(startLoc, destinationLoc, callback) {
.....
this.setState({ routebox: data },callback());
}
or but not sure if it would be in order:
async () => {
await this.getDirections((this.state.startingLocation.latitude + "," + this.state.startingLocation.longitude), (this.state.finalDestination.latitude + "," + this.state.finalDestination.longitude) );
this.resetRouteSelectionStatus();
}
You might need edit the resetRouteSelectionStatus to be:
resetRouteSelectionStatus = async ()=>{

How do I render/animate out items that leave array

Currently, I have a scraper that scrapes slack messages and stores them in a db somewhere.
On the frontend, I am pulling every second to see if new messages pop up. And then I render those messages on screen.
If anyone on slack replies or emotes on a message, the message gets removed from the backend thus getting removed from the frontend.
What I am trying to do now is when an item gets removed, I would like to animate it somehow.
Here is some of my current code:
async componentDidMount() {
await this.grab_channels()
await this.grab_slack_user_data()
await this.grab_items()
setInterval(() => {
this.grab_items()
}, this.state.settings.seconds_per_slack_messages_pull * 1000 )
}
grab_items() {
let url = this.state.settings.api_url + 'channel/' + this.state.selected_channel + '/now'
return new Promise( resolve => {
axios.get( url )
.then( res => {
this.setState( { items: res.data } )
resolve()
} )
})
}
And finally, items get rendered:
this.props.items.map( t => {
return (
<Item
key={ t.usr + '_' + t.ts }
task={ t }
user={ this.props.slack_users[ t.usr ] }
settings={ this.props.settings }
now={ this.state.now }
/>
)
} )
I was thinking of doing some sort of check within grab_items() but I wouldn't know how to continue after that. It would be easy to determine which ones should be rendered out but the problem is actually doing it.
Anyone have experience building something like this out?
Thanks!
Using Transition Groups is one way to do this:
https://github.com/reactjs/react-transition-group
Take a look at this example:
https://reactcommunity.org/react-transition-group/transition-group
For the check part in your function grab_items
/* include "clone" so that we don't modify state directly */
import clone from 'clone'
grab_items() {
let url = this.state.settings.api_url + 'channel/' + this.state.selected_channel + '/now'
return new Promise(resolve => {
axios.get(url).then(res => {
/* figure out what items to remove before you set the state */
let itemsToShow = []
for (let i = 0; i < this.state.items.length; i++) {
let ifFound = false
let t = clone(this.state.items[i])
for (let j = 0; j < res.data.length; j++) {
if (t.key === res.data[j].key) {
ifFound = true
}
}
/* if ifFound is false, it means it is not in the messages any more. */
if(!ifFound){
t.haveAnimation = true
itemsToShow.push(t)
}
itemsToShow = itemsToShow.concat(res.data)
this.setState(itemsToShow)
}
})
})
}
Then every second when it re-pull the data, you will have a list of items to show. The list has the items need to have the "disappear" animation and also it has the new messages.
To make the animation work, in the render part:
this.props.items.map(t => {
return (
<Item
key={t.usr + '_' + t.ts}
className={t.haveAnimation ? 'animationCSS' : ''}
task={t}
user={this.props.slack_users[t.usr]}
settings={this.props.settings}
now={this.state.now}
/>
)
}
Above code should attach the css class to the Item. You can put whatever css animation in the class

Resources