Mapping obsevable to another list of promise observable - javascript

Here what i am trying to do:
I have pokemons from the api. pokeapi
The http response is list of object only includes name and url prop.
I need to map these url to observable values. I can basically use
Promise.all(...).then(data => observable$.next(data))
but this seems unelegant to me,
here is what i try
const __URL__ = 'https://pokeapi.co/api/v2/pokemon/';
const pokemon$ = from(
fetch(__URL__)
.then((res) => res.json())
.then((res) => res.results)
);
var pokemons$ = new BehaviorSubject([]);
pokemon$.subscribe((pokes) => {
Promise.all(pokes.map((p) => fetch(p.url).then((res) => res.json()))).then((pokks) =>
pokemons$.next(pokks)
);
});
I just wonder is there a method to use observable operators(map) to generate the result with a single observable elegantly.

You can achieve that using forkJoin function, like the following:
// import { BehaviorSubject, forkJoin, from } from 'rxjs';
// import { mergeMap } from 'rxjs/operators';
const __URL__ = 'https://pokeapi.co/api/v2/pokemon/';
const pokemon$ = from(
fetch(__URL__)
.then((res) => res.json())
.then((res) => res.results as Array<{ name: string; url: string }>)
);
const pokemons$ = new BehaviorSubject([]);
pokemon$
.pipe(
mergeMap((pokes) =>
forkJoin(
pokes.map((poke) => from(fetch(poke.url).then((res) => res.json())))
)
)
)
.subscribe((pokks) => pokemons$.next(pokks));

Related

How to replace 'useResult' in graphql with vue and apolo?

I have to replace this useResult that is fetching data from graphql
const locationOptions = useResult(
result,
[],
({ getLocations }): Option[] => formatOptions(getLocations)
)
and I want to change it for a computed function like
const locationOptions = computed(() => result.value.getLocations ?? [])
I was trying to use a watch to run the function but it seems not to be working
watch(locationOptions, () => {
formatOptions(locationOptions.value)
})
any suggestions?
You can use the format function already in the computed function:
const locationOptions = computed(() => {
return result.value?.getLocations ? formatOptions(result.value.getLocations) || []
})

Array.map() doesn't render anything in React

I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);

What is the correct way to do an async action using redux observable and firebase?

so I have this epic
export const listenToCountryVisitsEpic = (action$, store) => {
return action$.pipe(
ofType('UPDATE_TRIP_DETAILS'),
mergeMap((action) => {
const userId = store.value.user.id
const { country, newDetails } = action
const { people, places } = details
const scheme = {
people,
places,
}
firebase.database().ref(`users/${userId}/path`).update(scheme)
return mergeMap((response) => {
const payload = { ...JSON.parse(response.request.body), currentDetails: action.currentDetails }
return [updatedCountrySuccess(payload)]
})
}),
)
}
what I want to do is make the request to firebase and then if successful update redux which in turn will update react. if not successful then catch it
this is correctly posting to firebase but I need to catch errors correctly. I don't want to fire this: updatedCountrySuccess until I know it's a success or failure in firebase
can I use async/await for this?
You can use from() to turn Promise into Observable and then catchError to replace the error with whatever you want.
import { from } from 'rxjs';
import { catchError } from 'rxjs/operators';
...
return action$.pipe(
ofType('UPDATE_TRIP_DETAILS'),
mergeMap((action) => {
const userId = store.value.user.id
const { country, newDetails } = action
const { people, places } = details
const scheme = {
people,
places,
}
return from(firebase.database().ref(`users/${userId}/path`).update(scheme))
.pipe(
// `mergeMap` will handle only `next` notifications
mergeMap((response) => {
const payload = ...
return [updatedCountrySuccess(payload)]
}),
catchError(error => of(errorAction)),
);
}),
)

action creator does not return value to stream in marble test

I've got following Epic which works well in application, but I can't get my marble test working. I am calling action creator in map and it does return correct object into stream, but in the test I am getting empty stream back.
export const updateRemoteFieldEpic = action$ =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
const { orderId, fields } = payload;
const requiredFieldIds = [4, 12]; // 4 = Name, 12 = Client-lookup
const requestData = {
id: orderId,
customFields: fields
.map(field => {
return (!field.value && !requiredFieldIds.includes(field.id)) ||
field.value
? field
: null;
})
.filter(Boolean)
};
if (requestData.customFields.length > 0) {
return from(axios.post(`/customfields/${orderId}`, requestData)).pipe(
map(() => queueAlert("Draft Saved")),
catchError(err => {
const errorMessage =
err.response &&
err.response.data &&
err.response.data.validationResult
? err.response.data.validationResult[0]
: undefined;
return of(queueAlert(errorMessage));
})
);
}
return of();
})
);
On successfull response from server I am calling queueAlert action creator.
export const queueAlert = (
message,
position = {
vertical: "bottom",
horizontal: "center"
}
) => ({
type: QUEUE_ALERT,
payload: {
key: uniqueId(),
open: true,
message,
position
}
});
and here is my test case
describe("updateRemoteFieldEpic", () => {
const sandbox = sinon.createSandbox();
let scheduler;
beforeEach(() => {
scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});
afterEach(() => {
sandbox.restore();
});
it("should return success message", () => {
scheduler.run(ts => {
const inputM = "--a--";
const outputM = "--b--";
const values = {
a: updateRemoteField({
orderId: 1,
fields: [{ value: "test string", id: 20 }],
update: true
}),
b: queueAlert("Draft Saved")
};
const source = ActionsObservable.from(ts.cold(inputM, values));
const actual = updateRemoteFieldEpic(source);
const axiosStub = sandbox
.stub(axios, "post")
.returns([]);
ts.expectObservable(actual).toBe(outputM, values);
ts.flush();
expect(axiosStub.called).toBe(true);
});
});
});
output stream in actual returns empty array
I tried to return from map observable of the action creator which crashed application because action expected object.
By stubbing axios.post(...) as [], you get from([]) in the epic - an empty observable that doesn't emit any values. That's why your mergeMap is never called. You can fix this by using a single-element array as stubbed value instead, e.g. [null] or [{}].
The below is an answer to a previous version of the question. I kept it for reference, and because I think the content is useful for those who attempt to mock promise-returning functions in epic tests.
I think your problem is the from(axios.post(...)) in your epic. Axios returns a promise, and the RxJS TestScheduler has no way of making that synchronous, so expectObservable will not work as intended.
The way I usually address this is to create a simple wrapper module that does Promise-to-Observable conversion. In your case, it could look like this:
// api.js
import axios from 'axios';
import { map } from 'rxjs/operators';
export function post(path, data) {
return from(axios.post(path, options));
}
Once you have this wrapper, you can mock the function to return a constant Observable, taking promises completely out of the picture. If you do this with Jest, you can mock the module directly:
import * as api from '../api.js';
jest.mock('../api.js');
// In the test:
api.post.mockReturnValue(of(/* the response */));
Otherwise, you can also use redux-observable's dependency injection mechanism to inject the API module. Your epic would then receive it as third argument:
export const updateRemoteFieldEpic = (action$, state, { api }) =>
action$.pipe(
ofType(UPDATE_REMOTE_FIELD),
filter(({ payload: { update = true } }) => update),
mergeMap(({ payload }) => {
// ...
return api.post(...).pipe(...);
})
);
In your test, you would then just passed a mocked api object.

How to request data sequentially in Cycle.js?

I’m new to reactive programming and toying around with cycle.js, trying to implement who to follow box from this tutorial. But I understood that for proper implementation (and learning purposes) I don’t have one piece of data: full user name. I can get it by sequentially getting users and then full user data from server. In imperative style I would do something like this:
fetch(`https://api.github.com/users`)
.then(data => data.json())
.then(users => fetch(users[0].url))
.then(data => data.json())
.then(/* ... work with data ... */)
But how do I do it in cycle?
I’m using fetch driver and trying something like this:
function main({ DOM, HTTP }) {
const users = `https://api.github.com/users`;
const refresh$ = DOM.select(`.refresh`).events(`click`)
const response$ = getJSON({ key: `users` }, HTTP)
const userUrl$ = response$
.map(users => ({
url: R.prop(`url`, R.head(users)),
key: `user`,
}))
.startWith(null)
const request$ = refresh$
.startWith(`initial`)
.map(_ => ({
url: `${users}?since=${random(500)}`,
key: `users`,
}))
.merge(userUrl$)
const dom$ = ...
return {
DOM: dom$,
HTTP: request$,
};
}
where getJSON is
function getJSON(by, requests$) {
const type = capitalize(firstKey(by));
return requests$
[`by${type}`](firstVal(by))
.mergeAll()
.flatMap(res => res.json());
And I’m always getting some cryptic (for me) error like: TypeError: Already read. What does it mean and how do I handle it properly?
You were quite close. You just need to remove startWith(null) as a request, and grabbing the second response (you were missing the getJSON for that one).
function main({ DOM, HTTP }) {
const usersAPIPath = `https://api.github.com/users`;
const refresh$ = DOM.select(`.refresh`).events(`click`);
const userResponse$ = getJSON({ key: `user` }, HTTP);
const listResponse$ = getJSON({ key: `users` }, HTTP);
const userRequest$ = listResponse$
.map(users => ({
url: R.prop(`url`, R.head(users)),
key: `user`,
}));
const listRequest$ = refresh$
.startWith(`initial`)
.map(_ => ({
url: `${usersAPIPath}?since=${Math.round(Math.random()*500)}`,
key: `users`,
}));
const dom$ = userResponse$.map(res => h('div', JSON.stringify(res)));
return {
DOM: dom$,
HTTP: listRequest$.merge(userRequest$),
};
}
Because inquiring minds want to know...here's a complete working example:
import Cycle from '#cycle/rx-run';
import {div, button, makeDOMDriver} from '#cycle/dom';
import {makeFetchDriver} from '#cycle/fetch';
import R from 'ramda'
function main({DOM, HTTP}) {
const usersAPIPath = 'https://api.github.com/users';
const refresh$ = DOM.select('button').events('click');
const userResponse$ = getJSON({ key: 'user' }, HTTP);
const listResponse$ = getJSON({ key: 'users' }, HTTP);
const userRequest$ = listResponse$
.map(users => ({
url: R.prop('url', R.head(users)),
key: 'user',
}));
const listRequest$ = refresh$
.startWith('initial')
.map(_ => ({
url: `${usersAPIPath}?since=${Math.round(Math.random()*500)}`,
key: 'users',
}));
const dom$ = userResponse$.map(res => div([
button('Refresh'),
div(JSON.stringify(res))
]));
return {
DOM: dom$,
HTTP: listRequest$.merge(userRequest$)
};
function getJSON(by, requests$) {
return requests$.byKey(by.key)
.mergeAll()
.flatMap(res => res.json());
}
}
Cycle.run(main, {
DOM: makeDOMDriver('#main-container'),
HTTP: makeFetchDriver()
});
It took me a while to figure out HTTP was the #cycle/fetch driver, and NOT the #cycle/http driver. Next, a bit of searching turned the ramda npm library providing prop and head methods.

Categories

Resources