I am trying to check the validity of a function I have written in Typescript, in congruence with RxJS observables, that fetches some bookings from one service and then for each booking fetches its corresponding location and activity from another service.
I am simply writing this post to verify the validity of what I have written and to ask if there is anything I could have done more efficiently.
let params = new HttpParams();
params = params.append('status', 'C');
params = params.append('offset', offset.toString());
params = params.append('limit', limit.toString());
return this.http.get(`${this.environment.booking.url}/my/bookings`, { params }).pipe(
mergeMap((bookings: Booking[]) => {
if(bookings.length > 0) {
return forkJoin(
bookings.map((booking: Booking) =>
forkJoin(
of(booking),
this.activityService.getActivity(booking.activity),
this.locationService.getLocation(booking.finalLocation),
).pipe(
map((data: [ Booking, Activity, Location ]) => {
let booking = data[0];
booking.activityData = data[1];
booking.finalLocationData = data[2];
return booking;
})
)
)
)
}
return of([]);
}),
catchError((err: HttpErrorResponse) => throwError(err))
);
I am expecting for this function to return a list of bookings alongside their corresponding location and activity. However more importantly I want to verify that what I am doing is correct and sensible. Is there anything I could have done differently to make it cleaner/ more human-readable (not nit-picking, please 😁 )?
On a different note, that of performance, I also have a follow-up question with regards to performance. Given that a list of bookings has common activities and locations. Is there a way to only fetch activities and locations without any duplicate HTTP requests? Is this already handled under the hood by RxJS? Is there anything I could have done to make this function more efficient?
I'm not sure about the efficiency, but, at least for me, it was a little hard to read
Here's how I'd do it:
I used a dummy API, but I think it correlates with your situation
const usersUrl = 'https://jsonplaceholder.typicode.com/users';
const todosUrl = 'https://jsonplaceholder.typicode.com/todos';
const userIds$ = of([1, 2, 3]); // Bookings' equivalent
userIds$
.pipe(
filter(ids => ids.length !== 0),
// Flatten the array so we can avoid another nesting level
mergeMap(ids => from(ids)),
// `concatMap` - the order matters!
concatMap(
id => forkJoin(ajax(`${usersUrl}/${id}`), ajax(`${todosUrl}/${id}`))
.pipe(
map(([user, todo]) => ({ id, user: user.response, todo: todo.response }))
)
),
toArray()
)
.subscribe(console.log)
Here is a StackBlitz demo.
With this in mind, here is how I'd adapt it to your problem:
this.http.get(`${this.environment.booking.url}/my/bookings`, { params }).pipe(
filter(bookings => bookings.length !== 0),
// Get each booking individually
mergeMap(bookings => from(bookings)),
concatMap(
b => forkJoin(
this.activityService.getActivity(b.activity),
this.locationService.getLocation(b.finalLocation),
)
.pipe(
map(([activity, location]) => ({ ...b, activity, location }))
)
),
// Getting the bookings array again
toArray()
catchError((err: HttpErrorResponse) => throwError(err))
);
This is how I would tackle this using RxJS:
Fetch all the Bookings
For Each Booking fetch Location and Activities cuncurrently
const { from, of, forkJoin, identity } = rxjs;
const { mergeMap, tap, catchError } = rxjs.operators;
const api = 'https://jsonplaceholder.typicode.com';
const endpoints = {
bookings: () => `${api}/posts`,
locations: (id) => `${api}/posts/${id}/comments`,
activities: (id) => `${api}/users/${id}`
};
const fetch$ = link => from(fetch(link)).pipe(
mergeMap(res => res.json()),
catchError(() => from([])),
);
fetch$(endpoints.bookings()).pipe(
mergeMap(identity),
mergeMap(booking => forkJoin({
booking: of(booking),
locations: fetch$(endpoints.locations(booking.id)),
activities: fetch$(endpoints.activities(booking.userId)),
})),
).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.js" integrity="sha256-Nihli32xEO2dsnrW29M+krVxoeDblkRBTkk5ZLQJ6O8=" crossorigin="anonymous"></script>
note:
Reactive Programming, and more generically declarative approaches, focus on avoiding imperative control flows... You should try to write your pipes without conditions (or any other control flow). To discard empty bookings you can use the filter operator.
Avoid nesting streams because this comes at the cost of readability.
forkJoin also takes a spec object which is very useful (part of the overloads)
Related
In my database, there are files. every single file has 'IS_DELETE' row, which shows whether this file is deleted or not.
If 'IS_DELETE' row of a file is 0, it means it is not deleted by the writer of the post.
If 'IS_DELETE' row of a file is 1, it means it is deleted by the writer.
However, I use a GET request to render the items.
I made the code so that you can see it clearly. I might mispelled!
test = () => {
const [files, setFiles] = useState([]);
const getDetails = async () => {
await get(`URL`)
.then((res) => {
setFiles(res)
})
}
useEffect=(() => {
getDetails()
},[])
return (
<>
files.map((file) => (
<div>
{file}
</div>
))
</>
)}
With this code, I render every file, although some files have '1' in their IS_DELETE row.
I am wondering whether I can conditionally get the items from DB
or
I get every file and filter my array using a particular function.
FYI, I get the files in this format.
[
{PK:1, NAME:'Hello.jpg', IS_DELETE: 0}
{PK:2, NAME:'Nice.jpg', IS_DELETE: 1}
{PK:3, NAME:'To.jpg', IS_DELETE: 0}
]
Thank you. I'm not good at English. So please be understandable.
Waiting for your wisdom!
No, you cannot modify the response you get from server unless it specifically has an endpoint to do so. It's like sending a letter and awqiting the response - you cannot make the response diffeent unless your recipient allows you to send a letter with a specific request (eg. 'give me only non-deleted items'), but if you're not in charge of the server, the only thing you can do is indeed filtering the items you get in response.
const getDetails = async () => {
await get(`URL`)
.then((res) => {
const filtered = res.filter(file => file.IS_DELETE === 0);
setFiles(filtered);
})
}
you can pass the parameter is_deleted in the url concatenate with the main one
{url}?is_deleted=0
Depending on the database you are using, it is possible to do so. For example, in an SQL database, you could do SELECT * FROM files WHERE IS_DELETE != 0
But you can also do it in the frontend as shown below:
const getDetails = async () => {
const res = await get(`URL`);
setDetails(res.filter(file => !file.IS_DELETE));
}
my idea is to pass parameter in the url as query string and in the server side you get the value and put it in the database query
test = () => {
const [files, setFiles] = useState([]);
const getDetails = async () => {
await get(`URL?is_deleted=0`)
.then((res) => {
setFiles(res)
})
}
useEffect=(() => {
getDetails()
},[])
return (
<>
files.map((file) => (
<div>
{file}
</div>
))
</>
)}
I am working on a react asp.net application. From the server, I return a list of items, ordered by date. Then, I normalize it using normalizr:
axios.get(url, { params: { userId, submissionId } })
.then(response => {
const notifications = new schema.Entity('notifications');
const normalizedData = normalize(response.data, [notifications]);
dispatch(
fetchNotificationsSuccess(normalizedData.entities.notifications)
);
})
.catch(error => { notificationDataOperationFailure(error) });
When I run this, the items are reordered by their key values starting from 1. I wonder how I can retain the order sent from the server.
You can find the order in the "result" you get, see more info here: https://github.com/paularmstrong/normalizr/issues/9
Thanks for reading.
I have two Collections in Firestore and I'm using Angularfire2.
I have a collections of "Clients" and a collection of "Jobs". Clients can have a number of Jobs and each job has a linked client.
I've created a component to show a list of all the jobs, and I'm trying to pull in the associated client for each job using the Firestore key.
Here's my data:
I have been able to "hack" a solution but it is extremely buggy, and it looses all async - so I might as well write a MySQL backend and forget Firestore - the whole point of using Firestore is it's "live".
public jobs = {};
[...]
processData() {
const clients = [];
this.clientCollection.ref.get().then((results) => {
results.forEach((doc) => {
clients[doc.id] = doc.data();
});
const jobs = {};
this.jobsCollection.ref.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
jobs[doc.id] = doc.data();
jobs[doc.id].id = doc.id;
jobs[doc.id].clientData = clients[doc.data().client];
});
this.jobs = jobs;
});
});
}
This works up to a point - but it strips out the async.
Key question: is there any way of doing a "join" as we would in an SQL database to pull these two data sets together? And is there a way of doing it in a way that will keep the async nature of the data?
You can try to use Promise.all() to fetch multiple promises into one object. Here is a revised version of your method that should give the desired result if I am understanding your question correctly:
async processData() {
const clientCollection = await firebase.firestore().collection(`clients`).get();
const jobsCollection = await firebase.firestore().collection(`jobs`).get();
const clients = [];
clients.push(clientCollection);
clients.push(jobsCollection);
const res = await Promise.all(clients);
// res should equal a combination of both querySnapshots
}
I just recreated the collection variables to show what to add to the array, but Promise.all() takes in an array of promises and resolves all of them into one array as the get() method in firestore is a Promise. This is also using async/await. Hope this can be of help!
EDIT:
Since you are using AngularFire2, you should use their method for this.
In you component you are going to want to import the angularfire2/firestore module and use the Observable methods they provide:
first import the module: import { AngularFireStore } from 'angularfire2/firestore';
then provide it to your constructor:
constructor(
private firestore: AngularFireStore
) { }
Then you could use Observable.combineLatest() to receive all the data at one time:
clients$: Observable<any[]>;
clients: any[];
jobs$: Observable<any[]>;
jobs: any;
joined: any;
ngOnInit() {
this.clients$ = this.getCollection('clients');
this.jobs$ = this.getCollection('jobs');
this.joined = Observable.combineLatest(this.clients$, this.jobs$);
this.joined.subscribe(([clients, jobs]) => {
this.clients = clients;
this.jobs = jobs;
});
}
getCollection(collectionName) {
return this.firestore.collection(`${collectionName}`).valueChanges();
}
In your markup you would just loop the data using *ngFor:
<div *ngFor="let client of clients">{{ client.name }}</div>
This way, your component will be listening for new data once firestore has the data and it will all come in at one time so you don't have nested subscriptions that are vulnerable to creating multiple subscriptions. Hope this can be of help.
late for solutions but after 4 hours searching I found this solution library its very useful for join collection and get data.
main resource : https://github.com/AngularFirebase/133-firestore-joins-custom-rx-operators
first create one type script file
file name : collectionjoin.ts
import { combineLatest, pipe, of, defer } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
export const leftJoinDocument = (afs: AngularFirestore, field, collection) => {
return source =>
defer(() => {
// Operator state
let collectionData;
const cache = new Map();
return source.pipe(
switchMap(data => {
// Clear mapping on each emitted val ;
cache.clear();
// Save the parent data state
collectionData = data as any[];
const reads$ = [];
let i = 0;
for (const doc of collectionData) {
// Skip if doc field does not exist or is already in cache
if (!doc[field] || cache.get(doc[field])) {
continue;
}
// Push doc read to Array
reads$.push(
afs
.collection(collection)
.doc(doc[field])
.valueChanges()
);
cache.set(doc[field], i);
i++;
}
return reads$.length ? combineLatest(reads$) : of([]);
}),
map(joins => {
return collectionData.map((v, i) => {
const joinIdx = cache.get(v[field]);
return { ...v, [field]: joins[joinIdx] || null };
});
}),
tap(final =>
console.log(
`Queried ${(final as any).length}, Joined ${cache.size} docs`
)
)
);
});
};
after that in your homepage.ts or any page typescript file.
import {
AngularFirestore,
AngularFirestoreCollection,
AngularFirestoreDocument,
} from "#angular/fire/firestore";
jobs: any
constructor( public afStore: AngularFirestore ) { }
this.afStore.collection('Jobs').valueChanges().pipe(
leftJoinDocument(this.afStore, 'client', 'Clients'),
shareReplay(1)
).subscribe((response) => {
this.products = response;
})
in this step we are passing three arguments
this.afStore = this is object of lib.
'client' = this is key/id of jobs collections
'Clients' = this is collection name which we are join.
now last step Display result
<ion-list lines="full" *ngFor="let job of Jobs">
<ion-item button detail>
<ion-label class="ion-text-wrap">{{ job.Name }}</ion-label>
<p>{{ job.Clients.companyName}}</p>
</ion-item>
</ion-list>
finally this code given two collection record.
thank you.
After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.
Might not be an optimum solution but here its solve your problem.
combineLatest(
this.firestore.collection('Collection1').snapshotChanges(),
this.firestore.collection('Collection2').snapshotChanges(),
//In collection 2 we have document with reference id of collection 1
)
.pipe(
take(1),
).subscribe(
([dataFromCollection1, dataFromCollection2]) => {
this.dataofCollection1 = dataFromCollection1.map((data) => {
return {
id: data.payload.doc.id,
...data.payload.doc.data() as {},
}
as IdataFromCollection1;
});
this.dataofCollection2 = dataFromCollection2.map((data2) => {
return {
id: data2.payload.doc.id,
...data2.payload.doc.data() as {},
}
as IdataFromCollection2;
});
console.log(this.dataofCollection2, 'all feeess');
const mergeDataFromCollection =
this.dataofCollection1.map(itm => ({
payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
...itm
}))
console.log(mergeDataFromCollection, 'all data');
},
I have a problem that I don't know how to resolve.
I have two epics that do requests to api and update the store:
const mapSuccess = actionType => response => ({
type: actionType + SUCCESS,
payload: response.response,
});
const mapFailure = actionType => error => Observable.of({
type: actionType + FAILURE,
error,
});
const characterEpic = (action$, store) =>
action$.ofType(GET_CHARACTER)
.mergeMap(({ id }) => {
return ajax(api.fetchCharacter(id))
.map(mapSuccess(GET_CHARACTER))
.catch(mapFailure(GET_CHARACTER));
});
const planetsEpic = (action$, store) =>
action$.ofType(GET_PLANET)
.mergeMap(({ id }) => {
return ajax(api.fetchPlanet(id))
.map(mapSuccess(GET_PLANET))
.catch(mapFailure(GET_PLANET));
});
Now I have a simple scenario where I would like to create the third action that combines the two above, let's call it fetchCharacterAndPlanetEpic. How can I do it?
I think in many cases (and in my) it's important that result of the first action is dispatched to the store before the second begins. That would be probably trivial to do with Promises and redux-thunk, but I can't somehow think of a way to do it with rxjs and redux-observable.
Thanks!
Tomasz's answer works and has pros and cons (it was originally suggested in redux-observable#33). One potential issue is that it makes testing harder, but not impossible. e.g. you may have to use dependency injection to inject a mock of the forked epic.
I had started typing up an answer prior to seeing his, so I figured I might as well post it for posterity in case it's interesting to anyone.
I also previously answered another question which is very similar that may be helpful: How to delay one epic until another has emitted a value
We can emit the getCharacter(), then wait for a matching GET_CHARACTER_SUCCESS before we emit the getPlanet().
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(({ characterId, planetId }) =>
action$.ofType(GET_CHARACTER_SUCCESS)
.filter(action => action.payload.id === characterId) // just in case
.take(1)
.mapTo(getPlanet(planetId))
.startWith(getCharacter(characterId))
);
One potential negative of this approach is that theoretically the GET_CHARACTER_SUCCESS this epic receives could be a different one the exact one we were waiting for. The filter action.payload.id === characterId check protects you mostly against that, since it probably doesn't matter if it was specifically yours if it has the same ID.
To truly fix that issue you'd need some sort of unique transaction tracking. I personally use a custom solution that involves using helper functions to include a unique transaction ID. Something like these:
let transactionID = 0;
const pend = action => ({
...action,
meta: {
transaction: {
type: BEGIN,
id: `${++transactionID}`
}
}
});
const fulfill = (action, payload) => ({
type: action.type + '_FULFILLED',
payload,
meta: {
transaction: {
type: COMMIT,
id: action.meta.transaction.id
}
}
});
const selectTransaction = action => action.meta.transaction;
Then they can be used like this:
const getCharacter = id => pend({ type: GET_CHARACTER, id });
const characterEpic = (action$, store) =>
action$.ofType(GET_CHARACTER)
.mergeMap(action => {
return ajax(api.fetchCharacter(action.id))
.map(response => fulfill(action, payload))
.catch(e => Observable.of(reject(action, e)));
});
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(action =>
action$.ofType(GET_CHARACTER_FULFILLED)
.filter(responseAction => selectTransaction(action).id === selectTransaction(responseAction).id)
.take(1)
.mapTo(getPlanet(action.planetId))
.startWith(getCharacter(action.characterId))
);
The key detail is that the initial "pend" action holds a unique transaction ID in the meta object. So that initial action basically represents the pending request and is then used when someone wants to fulfill, reject, or cancel it. fulfill(action, payload)
Our fetchCharacterAndPlanetEpic code is kinda verbose and if we used something like this we'd be doing it a lot. So let's make a custom operator that handles it all for us.
// Extend ActionsObservable so we can have our own custom operators.
// In rxjs v6 you won't need to do this as it uses "pipeable" aka "lettable"
// operators instead of using prototype-based methods.
// https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md
class MyCustomActionsObservable extends ActionsObservable {
takeFulfilledTransaction(input) {
return this
.filter(output =>
output.type === input.type + '_FULFILLED' &&
output.meta.transaction.id === input.meta.transaction.id
)
.take(1);
}
}
// Use our custom ActionsObservable
const adapter = {
input: input$ => new MyCustomActionsObservable(input$),
output: output$ => output$
};
const epicMiddleware = createEpicMiddleware(rootEpic, { adapter });
Then we can use that custom operator in our epic nice and cleanly
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(action =>
action$.takeFulfilledTransaction(action)
.mapTo(getPlanet(action.planetId))
.startWith(getCharacter(action.characterId))
);
The transaction-style solution described here is truly experimental. In practice there are some warts with it I've noticed over the years and I haven't gotten around to thinking about how to fix them. That said, overall it's been pretty helpful in my apps. In fact, it can also be used to do optimistic updates and rollbacks too! A couple years ago I made this pattern and the optional optimistic update stuff into the library redux-transaction but I've never circled back to give it some love, so use at your own risk. It should be considered abandoned, even if I may come back to it.
I've found a help in this github topic.
First I had to create helper method that will allow me to combine epics together:
import { ActionsObservable } from 'redux-observable';
const forkEpic = (epicFactory, store, ...actions) => {
const actions$ = ActionsObservable.of(...actions);
return epicFactory(actions$, store);
};
Which allows me to call any epic with stubbed actions like:
const getCharacter = id => ({ type: GET_CHARACTER, id });
forkEpic(getCharacterEpic, store, getCharacter(characterId))
...and will return result Observable of that epic. This way I can combine two epics together:
export const getCharacterAndPlanet = (characterId, planetId) => ({
type: GET_CHARACTER_AND_PLANET,
characterId,
planetId,
});
const fetchCharacterAndPlanetEpic = (action$, store) =>
action$.ofType(GET_CHARACTER_AND_PLANET)
.mergeMap(({ characterId, planetId }) =>
forkEpic(characterEpic, store, getCharacter(characterId))
.mergeMap((action) => {
if (action.type.endsWith(SUCCESS)) {
return forkEpic(planetsEpic, store, getPlanet(planetId))
.startWith(action);
}
return Observable.of(action);
})
);
In this example second request is called only if first ends with SUCCESS.
*SOLVED
The problem was in how I was creating and responding to the observable created by the firebase callback.
I also had way too much stuff going on inside my firebase callbacks.
I ended up splitting it up a bit more, using the firebase promise structure: https://firebase.googleblog.com/2016/01/keeping-our-promises-and-callbacks_76.html
and creating an Observable.fromPromise for the firebase callback within what is now called firebaseAPI.checkForUser.
*
Working epic:
export const contactFormFirebaseSubmitEpic = (action$) =>
action$.ofType(START_CONTACT_FORM_FIREBASE_SUBMIT)
.flatMap((firebaseSubmitAction) => {
const values = firebaseSubmitAction.values;
const formattedEmail = firebaseAPI.getFormattedEmail(values);
const contactsRef = firebaseAPI.getContactsRef(formattedEmail);
return firebaseAPI.checkForUser(values, formattedEmail, contactsRef);
})
.flatMap((data) => concat(
of(firebaseAPI.recordUserAndUpdateDetails(data))
))
.flatMap((data) => concat(
of(firebaseAPI.setQuoteData(data))
))
.switchMap((x) => merge(
of(stopLoading()),
of(contactFormFirebaseSuccess())
));
// original question
Ok so, what I'm trying to achieve is to perform the first action (firebaseAPI.checkUserAndUpdate), then the next, then when both of them are done essentially discard what's there and send out two actions (contactFormFirebaseSuccess and stopLoading).
This all works fine except for one weird thing, the setQuoteData function always runs before the checkUser function. Does anyone know why this might be?
Also if there's a better way to lay this out I'd be very open to suggestions! Cheers. Also I've taken out quite a few variables and things that would make it even more complicated. Basically I just wanted to show that in each case I'm returning an observable from 'doing something with firebase'. But I don't think that's the problem as I have console logs in each of the firebase functions and the setQuoteData one just fires first and then executes the firebase stuff then when it's done the checkUserAndUpdate one runs.
export const contactFormFirebaseSubmitEpic = action$ =>
action$.ofType(START_CONTACT_FORM_FIREBASE_SUBMIT)
.flatMap((firebaseSubmitAction) => {
const values = firebaseSubmitAction.values;
return merge(
firebaseAPI.checkUserAndUpdate(values),
firebaseAPI.setQuoteData(values),
)
.takeLast(1)
.mergeMap((x) => {
return merge(
of(contactFormFirebaseSuccess()),
of(stopLoading()),
);
});
});
const firebaseAPI = {
checkUserAndUpdate: (values) => {
const checkUserAndUpdateDetails = firebaseRef.once('value', snapshot => {
const databaseValue = snapshot.val();
checkUserExistsAndUpdateDetails(databaseValue, values);
});
return Observable.from(checkUserAndUpdateDetails);
},
setQuoteData: (value) => {
const setQuote = setQuoteData(values);
return Observable.from(setQuote);
},
};
const stopLoading = () => ({ type: STOP_BUTTON_LOADING });
const contactFormFirebaseSuccess = () => ({ type: SUCCESS });
checkUserAndUpdate: (values, contactsRef) => {
const checkUser$ = Observable.from(contactsRef.once('value').then(
snapshot => {
const databaseValue = snapshot.val();
checkUserExistsAndUpdateDetails(
values,
databaseValue,
contactsRef,);
})
);
return checkUser$;
},
const checkUserExistsAndUpdateDetails = (
values,
databaseValue,
contactsRef,
) => {
if (databaseValue) { console.log('user exists'); }
else {
console.log('new user, writing to database');
contactsRef.set({
name: values.name,
email: values.email,
phone: values.phone,
});
}
};
The problem is that merge does not maintain the order of the streams that you subscribe to, it simply emits events from any of the source streams regardless of what order they emit.
If you need to maintain order you should use concat instead of merge
i.e.
const values = firebaseSubmitAction.values;
return concat(
firebaseAPI.checkUserAndUpdate(values),
firebaseAPI.setQuoteData(values),
)
Side note, I don't know why you are using the of operator there, you already have Observables returned from your API so you can just pass those to merge or concat in this case.