RX.JS Redux Observable Multiple Get requests at same time - javascript

I am trying to set up an observable that currently receives an array of location IDs and then makes a get request for all of these at once and waits for the response for them all. Here is a sample:
const fetchPhotosEpic = action$ =>
action$.ofType(LOCATIONS_RECEIVED)
.map(action => action.payload)
.mergeMap((data) => {
let promiseArray = data.map(location => Observable.fromPromise(axios.get(photosUrl(location.id))))
return Observable.forkJoin(
promiseArray
)
})
.map(responses => responses.map((response) => response.data.location))
Where data looks like:
[
{
id: "aoeuaeu",
name: "Test"
},
...
]
The issue I have right now is I get a 404 on one of the requests and it's messing everything up. I am probably doing something wrong as I am just learning RX. Any help would be great!

You can try adding a catch to each call and returning a new observable with the error message, which should stop the forkJoin failing if one request fails. You can then either filter out the failures, or add logic to handle them in your final .map. eg.
const fetchPhotosEpic = action$ =>
action$.ofType(LOCATIONS_RECEIVED)
.map(action => action.payload)
.mergeMap((data) => {
let promiseArray = data.map(location => {
return Observable.fromPromise(axios.get(photosUrl(location.id)))
.catch(error => Observable.of({error}))
})
return Observable.forkJoin(
promiseArray
)
})
.filter(response => !Boolean(response.error))
.map(responses => responses.map((response) => response.data.location))

Related

Handling multiple API calls of a single API call

With my team we are trying to implement a command for a really common operation for the business logic but I'm having issues handling its implementation.
Basically:
We have to retrieve an array of objects (GET).
For each of that objects we have to retrieve (GET) another object inside its father.
For each of that sub-objects (childs) we have to check a condition and if it is the wanted condition we retrieve the child, otherwise we pass null.
Q: How do I handle multiple API calls that depends from a single API call without getting outside the CY chain?
This is my current implementation (doesn't works but kinda explains the wanted logic)
Cypress.Commands.add('myCommand', (sumCriteria: Function, anotherCriteria: Function) => {
// I only retrieve fathers with certain criteria
return cy.request('GET', fathersUrl).its('body').then(fatherObjects => {
return fatherObjects.filter(father => father.childs.length && father.childs.find(sumCriteria))
}).then(filteredFathers => {
filteredFathers.forEach(father => {
// For each father I retrieve a single child
const targetChildId = father.childs.find(sumCriteria).id;
// For each single child I retrieve its data and evaluate if it has the needed criteria
cy.request('GET', `${childsUrl}/${targetChildId}`)
.its('body')
.then(property => anotherCriteria(property))
})
});
})
Thanks in advance!
You almost have the correct pattern, but instead of returning results, put them on the queue.
Cypress does two things to make this work
in a custom command, it waits for any asynchronous commands to resolve
it returns whatever is on the queue at the last evaluation
Cypress.Commands.add('myCommand', (sumCriteria, anotherCriteria) => {
cy.request('GET', fathersUrl)
.its('body')
.then(fatherObjects => {
const filteredFathers = fatherObjects.filter(father => {
return father.childs.find(sumCriteria)
});
const results = []
filteredFathers.forEach(father => {
cy.request('GET', father) // waits for all these to resove
.its('body')
.then(property => anotherCriteria(property))
})
cy.then(() => results) // returns this last queued command
})
})
A reproducible example:
Cypress.Commands.add('myCommand', (sumCriteria, anotherCriteria) => {
const fathersUrl = 'https://jsonplaceholder.typicode.com/todos/1'
cy.request('GET', fathersUrl)
.then(() => {
// simulated url extraction
const filteredFathers = [
'https://jsonplaceholder.typicode.com/todos/2',
'https://jsonplaceholder.typicode.com/todos/3'
]
const results = []
filteredFathers.forEach(father => {
cy.request('GET', father)
.then(res => {
results.push(res.body.id)
})
});
cy.then(() => results)
});
})
cy.myCommand()
.should('deep.eq', [2,3]) // ✅ passes

React API calls not fully completing before rendering

I'm essentially brand new to React, so please bear with me if there are any strange or obvious mistakes that I am missing.
I am looking to create a small little project with the National Hockey League statistics API and unfortunately have run into an issue pretty early on.
Essentially, what this code is trying to do right now is to fetch all of the current team ID's through https://statsapi.web.nhl.com/api/v1/teams, and then loop through each team's response and use it's unique ID to store every player ID currently playing in the league https://statsapi.web.nhl.com/api/v1/teams/ID/roster.
The API requests seem to be working just fine, however I'm running into an issue where the check for loading seems to be functioning improperly. If I say, for example
{loading ? <p>Loading...</p> : <p>{playerList[0].jerseyNumber}</p>}
I am given a "Cannot read properties of undefined" error message.
Is the way I attempted to signify loading incorrect? Is there a better way to be certain that the API requests are 100% finished before accessing the data?
Here is my code:
function App() {
const [playerList, setPlayerList] = useState([]);
const [loading, setLoading] = useState(true);
useEffect (() => {
fetch("https://statsapi.web.nhl.com/api/v1/teams")
.then((teamsResponse) => teamsResponse.json())
.then((teamsData) => {
const teams = teamsData.teams;
teams.forEach((team) => {
fetch("https://statsapi.web.nhl.com/api/v1/teams/" + team.id + "/roster")
.then((teamResponse) => teamResponse.json())
.then((teamData) => {
const roster = teamData.roster
roster.forEach((player) => {
setPlayerList((oldList) => [...oldList, player]);
});
});
});
});
setLoading(false);
}, []);
return (
<div>
{loading ? <p>Loading...</p> : playerList && <p>{playerList[0].jerseyNumber}</p>}
</div>
);
}
You're calling setLoading(false) outside of the fetch call. That is going to invoke the fetch and call setLoading(false) without waiting for the promise to resolve.
If you want the loading indicator to be set to false only after all of the roster fetch calls then you'll need to use Promise.all with a then. where you set the loading indicator to false, such as:
const teams = teamsData.teams;
const rosterRequests = teams.map(team => {
fetch("https://statsapi.web.nhl.com/api/v1/teams/" + team.id + "/roster")
.then(teamResponse => teamResponse.json())
.then(teamData => {
// Update roster state
});
// This will be executed once all the team roster requests have completed
Promise.all(rosterRequests).then(() => setLoading(false));
You can read more about Promise.all here.
setLoading should be set to false when all promises are settled since there are nested requests, I have updated and tested the solution
Updated code
useEffect(() => {
fetch("https://statsapi.web.nhl.com/api/v1/teams")
.then((teamsResponse) => teamsResponse.json())
.then((teamsData) => {
const teams = teamsData.teams;
let arr = [];
const allRosterData = teams.map(async (team) => {
const rosterData = await fetch(
"https://statsapi.web.nhl.com/api/v1/teams/" + team.id + "/roster"
)
.then((teamResponse) => teamResponse.json())
.then((teamData) => {
const dt = [];
const roster = teamData.roster;
roster.forEach((player) => {
dt.push(player);
});
return dt;
});
arr = [...arr, ...rosterData];
return rosterData;
});
Promise.all(allRosterData)
.then(() => {
//Change state here when all promises are settled
setPlayerList(arr);
setLoading(false);
})
.catch((err) => {
console.error(err);
});
});
}, []);
You are setting loading to false before the data is loaded. Try putting setLoading(false); into
.then((teamData) => {
const roster = teamData.roster
roster.forEach((player) => {
setPlayerList((oldList) => [...oldList, player]);
});
setLoading(false); <---
});

React - Returning data from API

I know there are similar questions, but I can't find the answer.
First, please tell me if I'm doing something really wrong
I need to populate my state with data from an API call. This was working fine with code above:
export const GetPlanets = async () => {
const planets = await axios.get(`${BASE_URL}`).catch((e) => {
console.error(e);
})
return planets.data.results
}
But then I needed to make a second call to several links from one json response filed, and I managed to make it work (don't know if it is the correct approach, though)
const GetPlanets = async () => {
let planetas = {}
await axios.get(`${PLANETS_URL}`)
.then((p) => {
planetas = p.data.results
return axios.get(`${FILMS_URL}`)
}).then((f) => {
planetas.films.forEach((v, i) => {
planetas[i].film = f
})
})
})
.catch((e) => {
console.error(e);
})
return planetas
}
This is my component file, where I try to get the object, like I was doing before
useEffect(() => {
const fetchPlanetas = async () => { // ME TRYING...
const planetas = await GetPlanets()
setPlanetas(planetas)
setToShow(planetas[0])
};
fetchPlanetas()
}, [])
But all I get is undefined
You're getting an array of undefined because .map() needs a return value. In both your .map() callbacks, you are not returning anything.
const results = [1, 2, 3, 4]
const results2 = results.map(elem => {
elem = elem + 1
})
console.log(results2)
But, even if you did return something in your .map() callback, GetFilms(f) is asynchronous, so you would not get the results of GetFilms() mapped into the array as you would expect.
You have a couple of options:
If you have access to the API, send the films data along with the rest of the data when you do your first request.
Use async/await and Promise.all() to get responses.

React state in render is unavailable inside return

I have these methods that do some fetching, and then once done, they set the state. But the render is called before the state is done and does not update.
The below seems to work on it's own, but takes a minute to finish.
//returns an promise with Array
getTopIDs(url) {
return fetch(url).then(blob => blob.json()).then(json => json)
}
// makes a URL fetchs JSON and return promise with single ID
getStory(id) {
let url = `https://hacker-news.firebaseio.com/v0/item/${id}.json?print=pretty`
return fetch(url).then(blob => blob.json()).then(json => json)
}
// call above methods, set state when done
componentDidMount() { //
let arr = []
let promise = new Promise((resolve, reject) => {
let data = this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => {
this.getStory(id).then(res => {
arr.push(res)
})
})
//resolve once all pushed to arr
resolve(arr)
})
})
// set state once array is completed
promise.then(res => {
return this.setState({data: arr})
})
}
Then in the render below it logs 'no', 'no' and stops. Trying it outside the return it logs 'no','yes'. Searching other posts for this I tried setting a boolean when done and using the state callback but those did not work (full disclosure: I don't really understand the setState callback option)
render() {
return (
<div>
{
this.state.data.length
? console.log('yes')
: console.log('no')
}
</div>)
}
I need render to handle this.state.data only when done. How can I do it?
Add fiddle: https://jsfiddle.net/drumgod/e2atysu3/6/
Your method this.getStory() is async but your handling of the array creation is sync inside your promise.
You need to either use async/await or only run your resolve(arr) after idArr.forEach() is for sure completed (which may be easier to do using Promise.all(idArr.map(...)) where the ... is returning the result from this.getStory()).
This is how you'll want to set your state inside getStory:
this.setState(prevState => ({
data: [...prevState.data, res]
}))
As mentioned in the comments, this would render the component for each data point in the forEach.
In order to avoid this issue, this is how componentDidMount() should be formatted:
componentDidMount() {
const arr = [];
this.getTopIDs("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").then((idArr) => {
idArr.forEach((id, index) => this.getStory(id).then(res => arr.push(res)));
this.setState(prevState => ({ data: [...prevState.data, arr] }))
})
}
This also lets you get rid of the promise.then call at the end.

subscribe while preserving order

I am trying to implement a auto-reconnect client for a server which receives a command and then replies with a single byte. However, the problem is that one cannot send any additional commands to the server while it is processing commands. So I need to somehow serialise the commands, is that possible to achieve in a pragmatic way in RxJS?
const onClient = new BehaviourSubject(...) // Auto-reconnecting client
function sendCommand(cmd) {
return onClient
.concatMap(client => {
client.write(cmd + '\r\n')
return Rx.Observable.fromEvent(client, 'data').take(1)
})
}
sendCommand('CMD1').subscribe(x => console.log(x))
sendCommand('CMD2').subscribe(x => console.log(x)) // Oops, sent a command while another one is active...
Here is one possible solution that lacks error handling and looks quite inefficient.
const input = new Rx.Subject()
const output = new Rx.Subject()
input.concatMap(({cmd, id})) => onClient
.filter(client => client != null)
.concatMap(client => {
client.write(cmd + '\r\n')
return Rx.Observable.fromEvent(client, 'data').take(1)
})
.map(value => ({value, id}))
.subscribe(output)
function sendCommand(cmd) {
const id = cuid()
input.onNext(id)
return output
.filter(res => res.id === id)
.map(res => res.value)
}
Any better ideas or suggestions on improvement?
Here is my gut instinct. I've only ever used JavaRX, and that just barely. Note that this assumes you want 1 invocation of CMD2 for every return of CMD1.
const onClient = new BehaviourSubject(...) // Auto-reconnecting client
function sendCommand(cmd) {
return onClient
.concatMap(client => {
client.write(cmd + '\r\n')
return Rx.Observable.fromEvent(client, 'data').take(1)
})
}
sendCommand('CMD1').subscribe(function(x) {
console.log(x);
sendCommand('CMD2').subscribe(y => console.log(y))
});
For what it's worth, you may want to consider using Promises for this stuff. My understanding of Rx is that it is useful for complex streams of async data, such as event streams. But if all you want is the async part, I believe Promises might be easier. We were considering using it on a Java project and decided it wasn't what we needed. See: When to Use Rx
I don't know what you are working on, but a command-response pattern seems to me like it might be better served by Promises, especially if you expect the lambda you're passing into subscribe to only be invoked once.
Here is the rather complicated try I ended up with:
import stampit from 'stampit'
import Rx from 'rx'
import cuid from 'cuid'
let input = new Rx.Subject()
let output = new Rx.Subject()
input
.concatMap(({fn, id}) => Rx.Observable
.defer(() => fn())
.map(value => ({value, id}))
.catch(error => Rx.Observable.return({error, id}))
.concat(Rx.Observable.return({id})))
.subscribe(output)
async function enqueue(fn) {
const id = cuid()
input.onNext({fn, id})
output
.filter(res => res.id === id)
.takeWhile(res => res.error || res.value)
.concatMap(res => res.error
? Rx.Observable.throw(res.error)
: Rx.Observable.return(res.value))
}
})
const onClient = new BehaviourSubject(...) // Auto-reconnecting client
function sendCommand(cmd) {
return enqueue(() => onClient
.concatMap(client => {
client.write(cmd + '\r\n')
return Rx.Observable.fromEvent(client, 'data').take(1)
}))
}
sendCommand('CMD1').subscribe(x => console.log(x))
sendCommand('CMD2').subscribe(x => console.log(x))

Categories

Resources