How can I subscribe and unsubscribe from individual firestore queries? - ngrx

I am trying to modify an effect I have made into letting me start and stop multiple firestore queries by using two actions. Currently the effect allows me to start and stop a single firestore query by listening for two separate actions in the effect. I simply use a switchMap to switch into an empty observable when there is a stop action. This works just fine.
#Effect()
startStopQuery$ = this.actions$.pipe(
ofType(
ActionTypes.START,
ActionTypes.STOP
),
switchMap(action => {
if (action.type === ActionTypes.STOP) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', 'x');
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
What I actually want to do is to have multiple queries ongoing that I can start and stop with those same two actions, but where it depends on the action payload. When I modified it everytime I performed a new query the last one stops working. I think it is because the switchMap operator switches away from my last query observable. This is the best I have come up with:
#Effect()
startStopQueryById$ = this.actions$.pipe(
ofType(
ActionTypes.START_BY_ID,
ActionTypes.STOP_BY_ID
),
switchMap(action => {
if (action.type === ActionTypes.STOP_BY_ID) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', action.id);
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
As I said, I think the issue is the switchMap operator. But that is also what I depended on to make the "stop" work in the first place. I cant seem to come up with another solution as I am not very well versed in the style yet.
Any help would be greatly appreciated!

I came up with a solution. I make an object that maps ID's to the firestore statechanges observables. On the start action I make the listener and adds it to the object. I make sure that it automatically unsubscribe by piping takeUntil with the corresponding stop action. It returns a merge of all the observables in the object and I silply do as before. I also have a seperate effect triggered by the stop action to remove the observable from the object. It looks like so:
queriesById: {[id: string]: Observable<DocumentChangeAction<Element>[]>} = {};
#Effect()
startQuery$ = this.actions$.pipe(
ofType(ActionTypes.START_BY_ID),
switchMap(action => {
this.queriesByPlay[action.pid] = this.afs.collection<Element>('requests', ref => {
return ref.where('field', '==', action.id);
}).stateChanges().pipe(
takeUntil(
this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
filter(cancelAction => action.id === cancelAction.id),
)
)
);
return merge(
Object.values(this.queriesByPlay)
);
}),
mergeMap(actions => actions),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
Effect({dispatch: false})
stopQuery$ = this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
map(action => delete this.queriesByPlay[action.id]),
);
This seems to work and have no problems except for being convoluted hard to understand.

Related

Effect "RouterEffects.navigate$" dispatched an invalid action: {"path":["..."]}

I'm using #ngrx/router-store and setting up a CustomerSerializer to manage routes in my state tree. I'm using the example in the #ngrx/router-store configuration docs. Also setup a RouterEffects class with a navigate$ effect to a specified path:
navigate$ = createEffect(():any => {
return this.actions$.pipe(
ofType(RouterActions.GO),
map((action: RouterActions.Go) => action.payload),
tap(({ path, query: queryParams, extras }) => {
this.router.navigate(path, { queryParams, ...extras });
})
)
}
);
When I dispatch a "GO" action with a payload, the Angular router navigates to the specified path, but I get this error in the console:
ERROR Error: Effect "RouterEffects.navigate$" dispatched an invalid action: {"path":["/admin"]}
I don't understand where the secondary action is being dispatched.
The problem is map((action: RouterActions.Go) => action.payload),
An effect always dispatches the next value back to the store.
This should be an action, but in this case it's the action's payload, resulting in the error.
To fix this you need to create non-dispatching effect.
navigate$ = createEffect(():any => {
return this.actions$.pipe(
ofType(RouterActions.GO),
map((action: RouterActions.Go) => action.payload),
tap(({ path, query: queryParams, extras }) => {
this.router.navigate(path, { queryParams, ...extras });
})
)
}
// add this 👇
, {dispatch: false}
);

Need to show newly added records/rows in Angular11 using web api service without page refresh

Component.ts
ngOnInit() {
this.employeeservice.getEmp_ServiceFun().subscribe(
(data: Employee[]) => this.employees = data,
error => this.error = error
);
}
service.ts
constructor(private http: HttpClient) { }
public getEmp_ServiceFun(): Observable<Employee[]> {
return this.http.get<Employee[]>(this.serverUrl + 'employees')
.pipe(
catchError(this.handleError)
);
}
it seems a bit strange to me to run the ngoninit method again since it is meant to run only once. I would wrap the employeeservice method in an observable interval. dont forget to unsubscribe though. otherwise it would keep calling the getEmp_ServiceFun until the whole app closes
ngOnInit() {
interval(1000).pipe(
map(() => {this.employeeservice.getEmp_ServiceFun().subscribe(
(data: Employee[]) => this.employees = data,
error => this.error = error
);})}
this would replace the whole array instead of just adding to it though. I would take a second look at the getEmp_ServiceFun so that you can ask only for new data and not all data and then push it to the array.
edit: even better would be to not subscribe in the the map but move it to after the pipe. you might need to use a switchMap
I used setTimeout() to refresh a component, it is working fine, but now I just need to check, is it good practice or not?
ngOnInit() {
this.employeeservice.getEmp_ServiceFun().subscribe(
(data: Employee[]) => this.employees = data,
error => this.error = error
);
//refresh this component
this.timeout = setTimeout(() => { this.ngOnInit() }, 1000 * 1)
}

combineLatest is not getting called

I have the following code
I am new to rjxs programming so wondering what can be the root cause. I am trying to do a certain task and first combine all latest result, then subscribe to assign results. But combineLatest is not working seems like. What can be the cause ?
const downloadUrls$: any = Array.from(event.target.files).map((file: any) => {
const fileName = ...
const path = ...
const fileRef = ....
const task: any = ...
return task.snapshotChanges().pipe(
filter(snap => snap.state === firebase.storage.TaskState.SUCCESS),
switchMap(() => {
return fileRef.getDownloadURL().pipe(
map((url: string) => ({
name: fileName,
filepath: url,
relativePath: path,
}))
);
})
);
});
combineLatest(...downloadUrls$).subscribe((urls: any) => {
console.log(hi); // not printing
});
The reason behind that combinelatest doesnt fire is te reference of the subject.
You should change to this version. So removing the ... before the downloadUrls$.
combineLatest(downloadUrls$).subscribe((urls: any) => {
console.log(hi); // not printing
});
Combinelatest is for merging two or more observables. Is there any specific reason using combinelatest with one observable? Otherwise you can just change to simpler version like
downloadUrls$.subscribe((urls: any) => {
console.log(hi); // not printing
});
The latest versions of cobineLateste() receive an array as a parameter:
combineLatest(downloadUrls$).subscribe((urls: any) => {
console.log(hi); // not printing
});

cannot correctly fetch firebase data via ngrx effect

I seek help from you for the first time as I am in deep trouble.
I am using ngrx effects to load some shop items into the cart on app init from firebase realtime database. As the payload I get the whole firebase database snapshot object instead of just the shop items themselves, therefore the ngrx store receives an object it cannot understand and the app state does not change.
Check the photos please: when console.logged(), I see the exact objects that I need. But the Redux Devtools reveal the real deal, and I do not know how to fix this. Could somebody give me advice? Thank you very, very much.
The effect looks like this:
#Effect()
getShopItems: Observable<any> = this.actions.pipe(
ofType(shopActions.GET_ITEMS),
switchMap(() => {
return of(this.database.ref('cart').once('value', snap => {
snap.forEach(childSnap => childSnap.val());
// snap.forEach(childSnap =>
// console.log(childSnap.val()));
}))
.pipe(
map((payload) => {
return {
type: 'GET_ITEMS_SUCCESS',
payload: payload
};
}
));
})
);
The reducer functions for the actions in question look like:
case shopActions.GET_ITEMS: {
return {
...state,
shopItems: [...state.shopItems],
itemCount: state.shopItems.length
};
}
case shopActions.GET_ITEMS_SUCCESS: {
return {
...state,
shopItems: [action.payload],
itemCount: state.shopItems.length + 1
};
}
https://imgur.com/a/pDGAwFI
Use from rather than of:
import { from } from 'rxjs';
#Effect()
getShopItems: Observable<any> = this.actions.pipe(
ofType(shopActions.GET_ITEMS),
switchMap(() => {
return from(this.database.ref('cart').once('value', snap => {
snap.forEach(childSnap => childSnap.val());
}))
.pipe(
map((payload) => {
return {
type: 'GET_ITEMS_SUCCESS',
payload: payload
};
}
));
})
);
RxJs from - https://www.learnrxjs.io/operators/creation/from.html

Share actions between domains

Usually I use redux-saga, but currently I need redux-thunk. I'm using ducks for modular structure and now for example I have two ducks: auth and user with async actions below:
auth-duck.js
register(credentials) {
return dispatch => {
dispatch(actions.registerRequest());
return service.requestRegister(credentials)
.then((response) => {
dispatch(actions.registerSuccess(...));
// Here I need to dispatch some action from user-duck.js
})
.catch(() => dispatch(actions.registerError(...)))
}
}
user-duck.js
fetchUser() {
return dispatch => {...}
}
I really don't know how to not mess these two modules and dispatch fetchUser after successful register.
I could return register result (e.g. token or something else) to container from here it was dispatched and then using chaining dispatch fetchUser.
AuthContainer.js
_onSubmit() {
this.props.register().then(() => this.props.fetchUser);
}
But I don't know is it the best way to manage such operations with redux-thunk?
There is no rule thunks can only make one HTTP request. If you need to fetch the user after login, then fetch it.
const login = credentials => dispatch => {
fetchLogin(credentials).then(() => {
dispatch({ type: "LoginSuccess" })
return fetchUser()
}).then(() => {
dispatch({ type: "UserFetched" })
})
}
If you want to reuse the user fetch action, then dispatch a thunk from a thunk.
const fetchCurrentUser = login => dispatch => {
return fetchUser(login.userId).then(user => {
dispatch({ type: "UserLoad" })
return user
})
}
const login = credentials => dispatch => {
return fetchLogin(credentials).then(login => {
dispatch({ type: "LoginSuccess" })
return dispatch(fetchCurrentUser(login))
}
}
In one of my apps, I call 7 action thunks after successful login.
After a long search I found two options how to share the logic from separate domains.
The first one is to use mapDispatchToProps (Thanks #DonovanM), just like this:
function mapDispatchToProps(dispatch) {
return {
login: (credentials) => {
return dispatch(authActions.login(credentials)).then(
() => dispatch(userActions.fetchUser())
);
}
}
}
login function returns Promise thats why we can chain it to another one.
And the second possible option:
Use something like a "bridge" file in our case it is app-sagas.js
app-duck.js
import {actions as authActions} from './auth-duck.js';
import {actions as userActions} from './user-duck.js';
export function doLogin(credentials) {
return dispatch => {
return dispatch(authAction.login(credentials)).then(
() => dispatch(userActions.fetchUser())
);
}
}
In the second case we can place all logic into ducks and avoid spreading the logic within containers. But I guess it is possible to combine both methods.

Resources