Proper way to access store in ngrx/effect - javascript

I am using Angular 6, ngrx/store, ngrx/effects.
I have an effect that should be triggered when i press "Save" button. I am using withLatestFrom there to collect all data what i need for sending it to the server:
#Effect({dispatch: false})
saveAll$ = this.actions$.pipe(
ofType(ActionTypes.Save),
withLatestFrom(
this.store.select(fromReducers.getData1),
this.store.select(fromReducers.getData2),
this.store.select(fromReducers.getData3),
this.store.select(fromReducers.getData4)
),
switchMap(([action, data1, data2, data3, data4]: [ActionType, Data1[], Data2[], Data3[], Data4[]]) => {
// here is some operations with these data
return this.apiService.saveData({data1, data2, data3, data4})
})
)
Here is getData1 selector:
export const getData1= createSelector(
getItems,
getIndexes,
(items, indexes) => {
console.log('HI, I AM getData1');
return transformItems(items, indexes);
}
);
getItems, in turn, return state.items. The problem is that state.items can be modified in another effect:
#Effect()
handleItemsChanges$ = this.actions$.pipe(
ofType(ActionTypes.ChangesInItems),
withLatestFrom(
this.store.select(fromReducers.getItems),
this.store.select(fromReducers.getUsers),
),
switchMap(([action, items, users]: [ActionType, Item[], User[]]) => {
console.log('I AM handleItemsChanges');
const actions = [];
if (itemsShouldBeUpdated) {
actions.push(new UpdateData(changes))
}
})
)
So getData1 selector gets data from the store depend on another effect named handleItemsChanges. handleItemsChanges effect is triggered every time something is changed related to the items and recalc it again.
As a result, in saveAll i am getting not actual state.items.
What am i doing wrong? May be i should use another operator insted of withLatestFrom or what ca be the solution? Thank you
P.S. Btw i am using withLatestFrom every time when i want to get some data from the store. Is it correct?

you need to have action handleItemsChanges fired before saveAll gets fired. One way to do it is to create an effect on handleItemsChanges action and trigger the save action.
The framework will guarantee the order of execution (handleItemsChanges first then save), this way the withLatestFrom operation will work as you expected.

I've found discussion on ngrx github : https://github.com/ngrx/platform/issues/467
Looks like we have 2 ugly variants for accessing store from effects now.

Related

Subscribe to observable, async map result with input from dialog, use result from map to route

I am calling an API-service which returns an Observable - containing an array of elements.
apiMethod(input: Input): Observable<ResultElement[]>
From this I have been choosing the first element of the array, subscribing to that. Then used that element to route to another page like this:
this.apiService
.apiMethod(input)
.pipe(map((results) => results[0])
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
This works just fine.
Problem is, I do not want to just use the first element, I want a MatDialog, or other similar to pop up, and give the user option of which element to choose, and THEN route to the correct one.
If the list only contain one element though, the dialog should not show, and the user should be routed immediately.
I have tried to open a dialog in the .pipe(map()) function, but the subscribe() happens before I get answer from the user, causing it to fail. And I am not sure if that even is the correct approach. How would any of you solve this problem?
Edit
Ended up doing partly what #BizzyBob suggested:
Changing map to switchmap in the API-call, making it this way:
this.apiService
.apiMethod(input)
.pipe(switchMap((results) => this.mapToSingle(results)
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
With the mapToSingle(ResultElement[]) being like this:
private mapToSingle(results: ResultElement[]): Observable<ResultElement> {
if (result.length === 1){
return of(results[0]);
}
const dialogConfig = new MatDialogConfig<ResultElement[]>();
dialogConfig.data = results;
const dialogRef = this.dialog.open(ResultDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
I would create a DialogComponent that takes in the list of choices as an input, and emits the chosen item when it's closed.
Then, create a helper method (maybe call it promptUser) that simply returns an observable that emits the selected value:
this.apiService.apiMethod(input)
.pipe(
switchMap(results => results.length > 1
? this.promptUser(results)
: of(results[0])
)
)
.subscribe(
result => this.router.navigate('elements/', result.id)
);
Here we simply use switchMap to return an observable that emits the proper item. If the length is greater than 1, we return the helper method that displays the dialog and emits the chosen item, else just emit the first (only) item. Notice that we wrapped plain value with of since within switchMap, we need to return observable.
In either case, the desired item is emitted and received by your subscribe callback.
Two possible options:
Having a subject for the selected result that is "nexted" either by user input or a side effect of getting an api result with one element.
Keeping track of an overall state of the component and responding appropriately whenever a selectedResult is set in the state.
The example below is a sketch of using an Observable to keep track of the component's state.
There are two input streams into the state, the results from the api and the user input for the selected result.
Each stream is converted into a reducer function that will modify the overall state.
The UI should subscribe to this state via an async pipe, showing the modal when appropriate, and updating updating state from events via the Subjects.
The redirection should come as an effect to the change of the state when selectedResult has a value.
readonly getResultsSubject = new Subject<MyInput>();
readonly resultSelectedSubject = new Subject<ResultType>();
private readonly apiResults$ = this.getResultsSubjects.pipe(
switchMap((input) => this.apiMethod(input))
);
readonly state = combineLatest([
this.apiResults$.pipe(map(results => (s) => results.length === 1
? { ...s, results, selectedResult: x[0], showModal: false }
: { ...s, results, showModal: results.length > 1 })),
this.resultSelectedSubject.pipe(map(selectedResult => (s) => ({ ...s, selectedResult })))
]).pipe(
scan((s, reducer) => reducer(s), { }),
shareReplay(1)
);
ngOnInit() {
this.state.pipe(
filter(x => !!x.selectedResult)
).subscribe(x => this.router.navigate('elements/', x.selectedResult.id));
}
I've been using this pattern a lot lately. It makes it pretty easy the number of actions and properties of the state grow.
I would solve it using the following method:
Get the data with your subscribe (without the pipe). And save this data in the component variable
options: any;
this.apiService
.apiMethod(input)
.subscribe(
(result) => {
if (result.length === 1) {
this.router.navigate([result[0]]);
return;
}
options = result;
}
)
with an ngIf on the modal (conditional of the length of the array of options > 0 display the component with the different choices when the data is received
<modal-component *ngIf="options.length > 0"></modal-component>
when the user (click) on an option inside your modal, use the router to redirect.
html
<div (click)="redirect(value)">option 1</div>
ts
redirect(value) {
this.router.navigate([value]);
}
That would be the most straight forward

Is there a better solution of this RxJS epic stream?

I have a redux state using redux-observable's epics.
I need to solve showing a message after a user deletes an object or more objects.
There are two ways how to delete an object:
by action deleteObject(id: string) which call deleteObjectFulfilled action
by action deleteObjects(ids: Array<string>) which call N * deleteObject(id: string) actions
I want to show only one message with a count of deleted messages after every success "deleting action".
My final solution of this epic is:
export const showDeleteInformationEpic = action$ =>
combineLatest(
action$.pipe(ofType(DELETE_OBJECT_FULFILLED)),
action$.pipe(
ofType(DELETE_OBJECTS),
switchMap(({ meta: { ids } }) =>
action$.pipe(
ofType(DELETE_OBJECT_FULFILLED),
skip(ids.length - 1),
map(() => ids.length),
startWith('BATCH_IN_PROGRESS'),
take(2),
),
),
startWith(1),
),
).pipe(
startWith([null, null]),
pairwise(),
map(([[, previousCount], [, currentCount]]) =>
(previousCount === 'BATCH_IN_PROGRESS')
? currentCount
: isNumber(currentCount) ? 1 : currentCount),
filter(isNumber),
map((count) => throwInfo('objectDeleted', { count })),
);
Can you see any better solution of this?
There is more simple solution if I use only deleteObjects(Array<string>) for both cases..
Instead of firing multiple actions, you can create and dispatch a single action DELETE_MULTIPLE and pass all the id(s) in the payload.
This way your effects will be a lot cleaner since you only have to subscribe to DELETE_MANY action and additionally, it will prevent multiple store dispatches.

Angular async pipe don't refreshes result after input array filtering

In a parent component I have a stream of Tour[] tours_filtered: Observable<Tour[]> which I assign in the subscribe function of an http request
this.api.getTours().subscribe(
result => {
this.tours_filtered = of(result.tours);
}
)
in the view I display the stream using the async pipe
<app-tour-box [tour]="tour" *ngFor="let tour of tours_filtered | async"></app-tour-box>
Up to here all works as expected. In a child component I have an input text which emits the value inserted by the user to filtering the array of Tour by title.
In the parent component I listen for the emitted values in a function, I switch to new stream of Tour[] filtered by that value using switchMap
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap(
(tours) => of( tours.filter((tour) => tour.name.toLowerCase().includes(term)) )
)
)
}
I thought that the async pipe was constantly listening to reflect the changes to the array to which it was applied and so I thought I didn't have to subscribe in the function above, but nothing change in the view when I type in the input to filtering the results.
The results are updating correctly if I assign the new stream to the original array in the subscribe function
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
).subscribe( val => { this.tours_filtered = of(val); })
}
Is this procedure correct? Could I avoid to subscribe because I already use the async pipe? There is a better way to reach my goal?
EDITED:
Maybe I found a solution, I have to reassing a new stream to the variable just like this
onSearchTitle(term: string) {
this.tours_filtered = of(this.city.tours).pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
);
}
and I don't need to subscribe again, the results in the view change according to the search term typed by the user. Is this the correct way?
I think in your situation the solution should work as follows:
onSearchTitle(term: string) {
this._searchTerm = term;
this.tours_filtered = of(
this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term))
)
}
Because in your example you don't change the observable which is used in ngFor. Thus it's not working.
However, I don't see the reason of using observables here unless this is the first step and you're going to fetch this data from server in future
UPDATE
The best solution for you would be to consider your input as an observable and watch for the changes:
// your.component.ts
export class AppComponent {
searchTerm$ = new BehaviorSubject<string>('');
results = this.search(this.searchTerm$);
search(terms: Observable<string>) {
return terms
.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => {
return of(this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term)))
}
)
)
}
}
// your.template.html
...
<input type="" (input)="searchTerm$.next($event.target.value)">
...
Additionally it would be great to add debounceTime and distinctUntilChanged for better user experience and less search requests.
See full example for the details. Also please, refer to this article for more detailed explanations

Nested subscribe and need all values to pass as body to API - Angular 6 RxJS

I am developing a table component and the data for the table component is to be populated on basis of three dropdown values. I need to pass in all three values to the API to get the desired response. I can achieve it using nested subscribes, which is a very bad way. but any change calls the API multiple times. How can I fix it? Most examples I found are for getting only the final subscribe value but in my case, I need all three. Any advice to achieve using tap and flatMap?
Please advice.
this._data.currentGroup.subscribe(bg => {
this.bg = bg;
this._data.currentUnit.subscribe(bu => {
this.bu = bu;
this._data.currentFunction.subscribe(jf => {
this.jf = jf;
this.apiService.getFunctionalData(this.bg, this.bu, this.jf)
.subscribe(
(data) => {
console.log(data)
}
);
});
});
});
This is what I did.
this._data.currentGroup.pipe(
tap(bg => this.bg = bg),
flatMap(bu => this._data.currentUnit.pipe(
tap(bu => this.bu = bu),
flatMap(jf => this._data.currentFunction.pipe(
tap(jf => this.jf = jf)
))
))
).subscribe();
This is a sample example of my dataService. I initialize my dataservice in the table component's constructor as _data.
changeGroup(bg: string) {
this.changeGroupData.next(bg);
}
private changeGroupData = new BehaviorSubject<string>('');
currentChangeGroup = this.changeGroupData.asObservable();
You can use combineLatest to combine the three Observables and then subscribe to all of them at once. It will emit a new value as soon as one of the three Observables changes.
combineLatest(this._data.currentGroup,
this._data.currentUnit,
this._data.currentFunction).subscribe(([bg, bu, jf]) => {
// do stuff
});
For an example, have a look at this stackblitz demo I created.

node.js server is hit multiple times into async function in React

I'm working with React, MongoDB, node.js and Express and this is my situation:
I have this piece of code inside my component:
renderWishlist(){
var quantity;
var itemID;
var tmp;
var myData = this.props.currentCart;
// console.log(myData.length) returns 3
for (var k=0; k<myData.length; k++){
tmp = myData[k];
quantity = tmp.quantity;
itemID = tmp.itemID;
this.props.fetchBook(itemID).then(function(book){
console.log("book: "+JSON.stringify(book));
});
}
}
myData is an object which holds a bunch of books info.
As you can see from my code above I'm iterating through all these books, retrieving the ID of the book and the available quantity, then I try to get other information (price, pages, etc...) for that particular book from another collection inside the same MongoDB database.
Once this piece of code is running I keep getting multiple logs like this inside chrome console:
book: {"type":"fetch_book","payload":{"data":{"_id":"58f6138d734d1d3b89bbbe31","chef":"Heinz von Holzen","title":"A New Approach to Indonesian Cooking","pages":"132","price":23,"image":"IndonesianCooking"},"status":200,"statusText":"OK","headers":{"content-type":"application/json; charset=utf-8","cache-control":"no-cache"},"config":{"transformRequest":{},"transformResponse":{},"timeout":0,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","maxContentLength":-1,"headers":{"Accept":"application/json, text/plain, */*"},"method":"get","url":"http://localhost:3001/books/58f6138d734d1d3b89bbbe31"},"request":{}}}
Which retrieves correctly the book but it seems to hit the server multiple times for the same book and I don't know why.
For completeness this is fetchBook action creator:
export function fetchBook(id){
const request = axios.get('http://localhost:3001/books/'+id);
return {
type: FETCH_BOOK,
payload: request
};
}
The reducer:
import {FETCH_BOOKS, FETCH_BOOK,FETCH_WISHLIST} from '../actions/types';
const INITIAL_STATE = { myBooks:[], currentCart:[], currentBook:[] };
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_BOOKS:
return { ...state, myBooks:action.payload };
case FETCH_BOOK:
return { ...state, currentBook:action.payload };
case FETCH_WISHLIST:
return { ...state, currentCart: action.payload };
default:
return state;
}
}
My node.js server call:
router.get('/books/:id', function(req, res, next){
Book.findById({_id:req.params.id}).then(function(book){
res.send(book);
}).catch(next);
});
Why the server is hit multiple times? If I have, let's say 3 books inside myData, I'd expect the server to be hit only 3 times.
Another question I have is: How can I make the for loop to wait for fetchBook action to finish before going on iterating the next myData item?
You say renderWishList is called from the render() method. Your call to this.props.fetchBook(itemID) is updating your state, which triggers a re-render, which calls this.props.fetchBook(itemID) and ad infinitum it goes. You can put a console.log() at the start of your render() method to confirm this.
I would call renderWishList() from your constructor() or your componentDidMount() method. And I would rename it to something like createWishList(), because you are not rendering it with this function, but creating the list which needs to be rendered.
Next, you will want to make sure you are updating your state correctly every time your call to fetchBook returns, and then you'll want to use that to render correctly.
Update State
I would change the FETCH_BOOKS reducer to:
case FETCH_BOOK:
return {
...state,
myBooks: [
...state.myBooks,
action.payload
]
};
This will add the book just fetched to the end of the array of book objects myBooks. I am not clear on what is books vs wishlist, so you may want to change the names I've used here. The idea is that when each loop of the for loop is done, your myBooks array in your state has each book that was passed in from this.props.currentCart.
Note, I'm not sure, but you may need to execute a dispatch inside the .then of your this.props.fetchBooks() call.
Render with State
I'm not sure how your are accessing your state, but probably you then want to take your state.myBooks, and map it to create a separate line item, which you can use in your render method. You do this by defining a const like this at the top of your render method:
const mappedBooks = state.myBooks.map(book =>
<div>Title: {book.title}</div>
);
You can then use {mappedBooks} in the return() of your render method where you want a list of the books in myBooks to show on the screen.
Async For Loop
Last, I wouldn't worry that you are running each fetchBook asynchronously. In fact, this is good. If you implement it so that each response updates the state, as I've suggested, then that will trigger a re-render each time and your screen will load with each book. Of course with a few books it will happen so fast it won't matter. But this is a very "React-y" way for it to work.
I can't figure out why your server is hit multiple times. But you can use bluebird npm for your second question. reduce function of bluebird will do exactly what you want.
You can see the documentation here: http://bluebirdjs.com/docs/api/promise.reduce.html
renderWishlist() {
var bluebird = require('bluebird')
// myData is an array
var myData = this.props.currentCart;
bluebird.reduce(myData, function(value, tmp) {
var quantity = tmp.quantity;
var itemID = tmp.itemID;
return this.props.fetchBook(itemID).then(function(book) {
console.log("book: "+JSON.stringify(book));
});
}, 0).then(function() {
// called when myData is iterated completely
});
}
This code should work for you.

Categories